From aa243789d5aaeac973a9e80a163d96e33e411dac Mon Sep 17 00:00:00 2001 From: Ramzi Sabra Date: Mon, 15 Jul 2024 04:58:24 +0300 Subject: [PATCH 1/2] Add Microsoft Entra ID example --- examples/microsoft.rs | 180 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 examples/microsoft.rs diff --git a/examples/microsoft.rs b/examples/microsoft.rs new file mode 100644 index 0000000..07b7640 --- /dev/null +++ b/examples/microsoft.rs @@ -0,0 +1,180 @@ +//! +//! This example showcases the process of integrating with the +//! [Microsoft OpenID Connect](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) +//! provider. +//! +//! Before running it, you'll need to create your own Microsoft Entra ID tenant (directory), register an application (client), and add a client secret. +//! +//! In order to run the example call: +//! +//! ```sh +//! TENANT_ID=xxx CLIENT_ID=yyy CLIENT_SECRET=zzz cargo run --example microsoft +//! ``` +//! +//! ...and follow the instructions. +//! + +use openidconnect::core::{ + CoreClient, CoreIdTokenClaims, CoreIdTokenVerifier, CoreProviderMetadata, CoreResponseType, +}; +use openidconnect::reqwest; +use openidconnect::{ + AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, + OAuth2TokenResponse, RedirectUrl, Scope, +}; +use url::Url; + +use std::env; +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpListener; +use std::process::exit; + +fn handle_error(fail: &T, msg: &'static str) { + let mut err_msg = format!("ERROR: {}", msg); + let mut cur_fail: Option<&dyn std::error::Error> = Some(fail); + while let Some(cause) = cur_fail { + err_msg += &format!("\n caused by: {}", cause); + cur_fail = cause.source(); + } + println!("{}", err_msg); + exit(1); +} + +fn main() { + env_logger::init(); + + let tenant_id = env::var("TENANT_ID").expect("Missing the TENANT_ID environment variable."); + let client_id = + ClientId::new(env::var("CLIENT_ID").expect("Missing the CLIENT_ID environment variable.")); + let client_secret = ClientSecret::new( + env::var("CLIENT_SECRET").expect("Missing the CLIENT_SECRET environment variable."), + ); + let issuer_url = IssuerUrl::new(format!( + "https://login.microsoftonline.com/{tenant_id}/v2.0" + )) + .unwrap_or_else(|err| { + handle_error(&err, "Invalid issuer URL"); + unreachable!(); + }); + + let http_client = reqwest::blocking::ClientBuilder::new() + // Following redirects opens the client up to SSRF vulnerabilities. + .redirect(reqwest::redirect::Policy::none()) + .build() + .unwrap_or_else(|err| { + handle_error(&err, "Failed to build HTTP client"); + unreachable!(); + }); + + // Fetch Microsoft's OpenID Connect discovery document. + let provider_metadata = CoreProviderMetadata::discover(&issuer_url, &http_client) + .unwrap_or_else(|err| { + handle_error(&err, "Failed to discover OpenID Provider"); + unreachable!(); + }); + + // Set up the config for the Microsoft OAuth2 process. + let client = + CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret)) + // This example will be running its own server at localhost:8080. + // See below for the server implementation. + .set_redirect_uri( + RedirectUrl::new("http://localhost:8080".to_string()).unwrap_or_else(|err| { + handle_error(&err, "Invalid redirect URL"); + unreachable!(); + }), + ); + + // Generate the authorization URL to which we'll redirect the user. + let (authorize_url, csrf_state, nonce) = client + .authorize_url( + AuthenticationFlow::::AuthorizationCode, + CsrfToken::new_random, + Nonce::new_random, + ) + // This example is requesting access to the "calendar" features and the user's profile. + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())) + .url(); + + println!("Open this URL in your browser:\n{}\n", authorize_url); + + let (code, state) = { + // A very naive implementation of the redirect server. + let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); + + // Accept one connection + let (mut stream, _) = listener.accept().unwrap(); + + let mut reader = BufReader::new(&stream); + + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); + + let redirect_url = request_line.split_whitespace().nth(1).unwrap(); + let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap(); + + let code = url + .query_pairs() + .find(|(key, _)| key == "code") + .map(|(_, code)| AuthorizationCode::new(code.into_owned())) + .unwrap(); + + let state = url + .query_pairs() + .find(|(key, _)| key == "state") + .map(|(_, state)| CsrfToken::new(state.into_owned())) + .unwrap(); + + let message = "Go back to your terminal :)"; + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", + message.len(), + message + ); + stream.write_all(response.as_bytes()).unwrap(); + + (code, state) + }; + + println!( + "Microsoft returned the following code:\n{}\n", + code.secret() + ); + println!( + "Microsoft returned the following state:\n{} (expected `{}`)\n", + state.secret(), + csrf_state.secret() + ); + + // Exchange the code with a token. + let token_response = client + .exchange_code(code) + .unwrap_or_else(|err| { + handle_error(&err, "No user info endpoint"); + unreachable!(); + }) + .request(&http_client) + .unwrap_or_else(|err| { + handle_error(&err, "Failed to contact token endpoint"); + unreachable!(); + }); + + println!( + "Microsoft returned access token:\n{}\n", + token_response.access_token().secret() + ); + println!("Microsoft returned scopes: {:?}", token_response.scopes()); + + let id_token_verifier: CoreIdTokenVerifier = client.id_token_verifier(); + let id_token_claims: &CoreIdTokenClaims = token_response + .extra_fields() + .id_token() + .expect("Server did not return an ID token") + .claims(&id_token_verifier, &nonce) + .unwrap_or_else(|err| { + handle_error(&err, "Failed to verify ID token"); + unreachable!(); + }); + println!("Microsoft returned ID token: {:?}", id_token_claims); +} From fb8c046e6c0658d1bad070e12cd09f582df57fb4 Mon Sep 17 00:00:00 2001 From: Ramzi Sabra Date: Mon, 15 Jul 2024 05:56:13 +0300 Subject: [PATCH 2/2] Add Microsoft example to Cargo.toml --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2cb0a61..e98f343 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,10 @@ required-features = ["reqwest-blocking"] name = "google" required-features = ["reqwest-blocking"] +[[example]] +name = "microsoft" +required-features = ["reqwest-blocking"] + [[example]] name = "okta_device_grant" required-features = ["reqwest-blocking"]