From 3abdfa3084fa04f60e40fabd809025cd5d1c3f41 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 13 Apr 2023 23:57:15 -0400 Subject: [PATCH 001/118] Add identity credentials --- Cargo.toml | 6 +- examples/oauth/main.rs | 25 ++- examples/oauth/signing_keys.rs | 4 - graph-core/Cargo.toml | 3 - graph-http/Cargo.toml | 3 - graph-oauth/Cargo.toml | 6 + graph-oauth/src/auth.rs | 25 ++- graph-oauth/src/identity/authority.rs | 81 ++++++++++ .../authorization_code_credential.rs | 145 ++++++++++++++++++ .../credentials/confidential_client.rs | 141 +++++++++++++++++ graph-oauth/src/identity/credentials/mod.rs | 9 ++ .../identity/credentials/token_credential.rs | 6 + .../src/identity/credentials/token_request.rs | 7 + graph-oauth/src/identity/mod.rs | 5 + graph-oauth/src/lib.rs | 2 + 15 files changed, 441 insertions(+), 27 deletions(-) create mode 100644 graph-oauth/src/identity/authority.rs create mode 100644 graph-oauth/src/identity/credentials/authorization_code_credential.rs create mode 100644 graph-oauth/src/identity/credentials/confidential_client.rs create mode 100644 graph-oauth/src/identity/credentials/mod.rs create mode 100644 graph-oauth/src/identity/credentials/token_credential.rs create mode 100644 graph-oauth/src/identity/credentials/token_request.rs create mode 100644 graph-oauth/src/identity/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ea06e256..5063a149 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,15 +36,15 @@ serde_json = "1" url = "2" lazy_static = "1.4.0" -graph-oauth = { path = "./graph-oauth", version = "1.0.0" } +graph-oauth = { path = "./graph-oauth", version = "1.0.0", default-features=false } graph-http = { path = "./graph-http", version = "1.1.0", default-features=false } graph-error = { path = "./graph-error", version = "0.2.2" } graph-core = { path = "./graph-core", version = "0.4.0" } [features] default = ["native-tls"] -native-tls = ["reqwest/native-tls", "graph-http/native-tls"] -rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls"] +native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls"] +rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls"] brotli = ["reqwest/brotli"] deflate = ["reqwest/deflate"] trust-dns = ["reqwest/trust-dns"] diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 337d6832..ae9d214e 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -1,18 +1,17 @@ +//! # Overview +//! +//! Most of these examples use a local server in order to listen for the redirect +//! after a user signs into microsoft. There are a few oauth flows that may use +//! other means of getting an access token such as the client credentials flow. +//! +//! # Setup +//! +//! In everyone of these examples you will first need to setup an application in the +//! azure portal. +//! +//! Microsoft Identity Platform: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-vs-authorization #![allow(dead_code, unused, unused_imports)] -/// # Overview -/// -/// Most of these examples use a local server in order to listen for the redirect -/// after a user signs into microsoft. There are a few oauth flows that may use -/// other means of getting an access token such as the client credentials flow. -/// -/// # Setup -/// -/// In everyone of these examples you will first need to setup an application in the -/// azure portal. -/// -/// Microsoft Identity Platform: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-vs-authorization - #[macro_use] extern crate serde; diff --git a/examples/oauth/signing_keys.rs b/examples/oauth/signing_keys.rs index a713762b..013f6372 100644 --- a/examples/oauth/signing_keys.rs +++ b/examples/oauth/signing_keys.rs @@ -3,7 +3,6 @@ use graph_rs_sdk::oauth::graph_discovery::{ }; use graph_rs_sdk::oauth::OAuth; -#[allow(dead_code)] fn get_signing_keys() { // Lists info such as the authorization and token urls, jwks uri, and response types supported. let signing_keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.signing_keys().unwrap(); @@ -20,7 +19,6 @@ fn get_signing_keys() { let _oauth: OAuth = GraphDiscovery::V1.oauth().unwrap(); } -#[allow(dead_code)] fn tenant_discovery() { let _oauth: OAuth = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) .oauth() @@ -28,7 +26,6 @@ fn tenant_discovery() { } // Using async -#[allow(dead_code)] async fn async_keys_discovery() { let signing_keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.async_signing_keys().await.unwrap(); @@ -39,7 +36,6 @@ async fn async_keys_discovery() { println!("{signing_keys2:#?}"); } -#[allow(dead_code)] async fn async_tenant_discovery() { let _oauth: OAuth = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) .async_oauth() diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index 1c3b46bf..c3ac90bd 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -7,9 +7,6 @@ license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" description = "Common types for the graph-rs-sdk crate" -keywords = ["onedrive", "microsoft", "microsoft-graph", "api"] -categories = ["web-programming::http-client"] - [dependencies] Inflector = "0.11.4" serde = { version = "1", features = ["derive"] } diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 1f8af05b..5d4b7679 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -7,9 +7,6 @@ license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" description = "Http client and utilities for the graph-rs-sdk crate" -keywords = ["onedrive", "microsoft", "microsoft-graph", "api", "oauth"] -categories = ["authentication", "web-programming::http-client"] - [dependencies] async-stream = "0.3" async-trait = "0.1.35" diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index c108c990..48717e35 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["microsoft", "oauth", "authentication", "authorization"] categories = ["authentication", "web-programming::http-client"] [dependencies] +async-trait = "0.1.35" base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" @@ -25,3 +26,8 @@ url = "2" webbrowser = "0.8.7" graph-error = { path = "../graph-error" } + +[features] +default = ["native-tls"] +native-tls = ["reqwest/native-tls"] +rustls-tls = ["reqwest/rustls-tls"] diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 602c2597..5b406645 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,6 +1,7 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; +use crate::identity::{Authority, AzureAuthorityHost}; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; @@ -365,6 +366,28 @@ impl OAuth { .refresh_token_url(&token_url) } + /// Set the authorization, access token, and refresh token URL + /// for OAuth based on a tenant id. + /// + /// # Example + /// ``` + /// # use graph_oauth::oauth::OAuth; + /// # let mut oauth = OAuth::new(); + /// oauth.tenant_id("tenant_id"); + /// ``` + pub fn authority(&mut self, host: &AzureAuthorityHost, authority: &Authority) -> &mut OAuth { + let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); + let auth_url = format!( + "{}/{}/oauth2/v2.0/authorize", + host.as_ref(), + authority.as_ref() + ); + + self.authorize_url(&auth_url) + .access_token_url(&token_url) + .refresh_token_url(&token_url) + } + /// Set the redirect url of a request /// /// # Example @@ -570,7 +593,7 @@ impl OAuth { /// use graph_oauth::oauth::OAuthCredential; /// /// let mut oauth = OAuth::new(); - /// oauth.generate_sha256_challenge_and_verifier(); + /// oauth.generate_sha256_challenge_and_verifier().unwrap(); /// /// # assert!(oauth.contains(OAuthCredential::CodeChallenge)); /// # assert!(oauth.contains(OAuthCredential::CodeVerifier)); diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs new file mode 100644 index 00000000..5bd44dc3 --- /dev/null +++ b/graph-oauth/src/identity/authority.rs @@ -0,0 +1,81 @@ +use url::Url; + +/// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). +/// Authentication libraries from Microsoft (this is not one) call this the +/// AzureCloudInstance enum or the Instance url string. +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum AzureAuthorityHost { + /// Custom Value communicating that the AzureCloudInstance. + Custom(String), + /// Microsoft Azure public cloud. Maps to https://login.microsoftonline.com + #[default] + AzurePublic, + /// Microsoft Chinese national cloud. Maps to https://login.chinacloudapi.cn + AzureChina, + /// Microsoft German national cloud ("Black Forest"). Maps to https://login.microsoftonline.de + AzureGermany, + /// US Government cloud. Maps to https://login.microsoftonline.us + AzureUsGovernment, +} + +impl AsRef<str> for AzureAuthorityHost { + fn as_ref(&self) -> &str { + match self { + AzureAuthorityHost::Custom(url) => url.as_str(), + AzureAuthorityHost::AzurePublic => "https://login.microsoftonline.com", + AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn", + AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de", + AzureAuthorityHost::AzureUsGovernment => "https://login.microsoftonline.us", + } + } +} + +impl TryFrom<AzureAuthorityHost> for Url { + type Error = url::ParseError; + + fn try_from(azure_cloud_instance: AzureAuthorityHost) -> Result<Self, Self::Error> { + Url::parse(azure_cloud_instance.as_ref()) + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Authority { + #[default] + AzureActiveDirectory, + AzureDirectoryFederatedServices, + /// Same as Aad. This is here since `common` is more familiar some times + Common, + Organizations, + Consumers, + TenantId(String), +} + +impl AsRef<str> for Authority { + fn as_ref(&self) -> &str { + match self { + Authority::AzureActiveDirectory | Authority::Common => "common", + Authority::AzureDirectoryFederatedServices => "adfs", + Authority::Organizations => "organizations", + Authority::Consumers => "consumers", + Authority::TenantId(tenant_id) => tenant_id.as_str(), + } + } +} + +impl ToString for Authority { + fn to_string(&self) -> String { + String::from(self.as_ref()) + } +} + +impl From<&str> for Authority { + fn from(value: &str) -> Self { + match value.as_bytes() { + b"common" => Authority::Common, + b"adfs" => Authority::AzureDirectoryFederatedServices, + b"organizations" => Authority::Organizations, + b"consumers" => Authority::Consumers, + _ => Authority::TenantId(value.to_owned()), + } + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs new file mode 100644 index 00000000..e01b145d --- /dev/null +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -0,0 +1,145 @@ +use crate::grants::GrantType; +use crate::identity::Authority; + +/// Creates an instance of the ClientSecretCredential with the details needed to authenticate +/// against Azure Active Directory with a prefetched authorization code. +/// +/// <param name="clientSecret">A client secret that was generated for the App Registration used to authenticate the client.</param> +/// <param name="authorizationCode">The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. +/// See https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow for more information.</param> + +#[derive(Clone)] +pub struct AuthorizationCodeCredential { + /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. + pub(crate) authorization_code: String, + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) client_secret: String, + pub(crate) redirect_uri: String, + pub(crate) scopes: Vec<String>, + /// The Azure Active Directory tenant (directory) Id of the service principal. + pub(crate) tenant_id: Authority, + pub(crate) code_verifier: Option<String>, +} + +impl AuthorizationCodeCredential { + pub fn new( + client_id: &str, + client_secret: &str, + authorization_code: &str, + redirect_uri: &str, + ) -> AuthorizationCodeCredential { + AuthorizationCodeCredential { + authorization_code: authorization_code.to_owned(), + client_id: client_id.to_owned(), + client_secret: client_secret.to_owned(), + redirect_uri: redirect_uri.to_owned(), + scopes: vec![], + tenant_id: Default::default(), + code_verifier: None, + } + } + + pub fn grant_type(&self) -> GrantType { + GrantType::AuthorizationCode + } + + pub fn builder(authorization_code: &str) -> AuthorizationCodeCredentialBuilder { + let credential_builder = AuthorizationCodeCredentialBuilder::create(authorization_code); + credential_builder + } +} + +pub struct AuthorizationCodeCredentialBuilder { + authorization_code_credential: AuthorizationCodeCredential, +} + +impl AuthorizationCodeCredentialBuilder { + pub fn create(authorization_code: &str) -> AuthorizationCodeCredentialBuilder { + Self { + authorization_code_credential: AuthorizationCodeCredential { + authorization_code: authorization_code.to_owned(), + client_id: Default::default(), + client_secret: Default::default(), + redirect_uri: Default::default(), + scopes: vec![], + tenant_id: Default::default(), + code_verifier: None, + }, + } + } + + pub fn with_redirect_uri( + &mut self, + redirect_uri: &str, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.redirect_uri = redirect_uri.to_owned(); + self + } + + pub fn with_client_id(&mut self, client_id: &str) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.client_id = client_id.to_owned(); + self + } + + pub fn with_client_secret( + &mut self, + client_secret: &str, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.client_secret = client_secret.to_owned(); + self + } + + pub fn with_tenant_id<T: Into<Authority>>( + &mut self, + tenant_id: T, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.tenant_id = tenant_id.into(); + self + } + + pub fn with_code_verifier( + &mut self, + code_verifier: &str, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.code_verifier = Some(code_verifier.to_owned()); + self + } + + pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scopes: I, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.scopes = + scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn build(&mut self) -> AuthorizationCodeCredential { + self.authorization_code_credential.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + use url::Url; + + #[test] + fn with_tenant_id_common() { + let credential = AuthorizationCodeCredential::builder("") + .with_tenant_id(Authority::TenantId("common".into())) + .build(); + + assert_eq!(credential.tenant_id, Authority::TenantId("common".into())) + } + + #[test] + fn with_tenant_id_adfs() { + let credential = AuthorizationCodeCredential::builder("") + .with_tenant_id(Authority::AzureDirectoryFederatedServices) + .build(); + + assert_eq!(credential.tenant_id.as_ref(), "adfs"); + } +} diff --git a/graph-oauth/src/identity/credentials/confidential_client.rs b/graph-oauth/src/identity/credentials/confidential_client.rs new file mode 100644 index 00000000..8dec6f05 --- /dev/null +++ b/graph-oauth/src/identity/credentials/confidential_client.rs @@ -0,0 +1,141 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::credentials::{AuthorizationCodeCredential, TokenCredentialOptions}; +use crate::identity::TokenRequest; +use crate::oauth::OAuthError; +use async_trait::async_trait; +use graph_error::{GraphFailure, GraphResult}; +use reqwest::Response; + +#[derive(Default)] +pub struct ConfidentialClient { + http_client: OAuth, + token_credential_options: TokenCredentialOptions, +} + +impl ConfidentialClient { + pub fn new<T>(credential: T, options: TokenCredentialOptions) -> GraphResult<ConfidentialClient> + where + T: TryInto<ConfidentialClient, Error = GraphFailure>, + { + let mut cred = credential.try_into()?; + cred.token_credential_options = options; + Ok(cred) + } +} + +#[async_trait] +impl TokenRequest for ConfidentialClient { + async fn get_token_silent(&self) -> GraphResult<Response> { + // self.http_client.build_async().authorization_code_grant(). + } +} + +impl TryFrom<AuthorizationCodeCredential> for ConfidentialClient { + type Error = GraphFailure; + + fn try_from(value: AuthorizationCodeCredential) -> Result<Self, Self::Error> { + let mut client = ConfidentialClient::default(); + + if value.authorization_code.trim().is_empty() { + return OAuthError::error_from(OAuthCredential::AuthorizeURL); + } + + if value.client_id.trim().is_empty() { + return OAuthError::error_from(OAuthCredential::ClientId); + } + + if value.client_secret.trim().is_empty() { + return OAuthError::error_from(OAuthCredential::ClientSecret); + } + + client + .http_client + .access_code(value.authorization_code.as_str()) + .client_id(value.client_id.as_str()) + .client_secret(value.client_secret.as_str()) + .redirect_uri(value.redirect_uri.as_str()) + .extend_scopes(value.scopes) + .authority( + &client.token_credential_options.azure_cloud_endpoint, + &value.tenant_id, + ); + + if let Some(code_verifier) = value.code_verifier.as_ref() { + client.http_client.code_verifier(code_verifier.as_str()); + } + + Ok(client) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::grants::{GrantRequest, GrantType}; + use crate::identity::credentials::AuthorizationCodeCredentialBuilder; + use url::Url; + + #[test] + fn test_auth_code_grant_serialization() { + let mut oauth = OAuth::new(); + oauth + .client_id("bb301aaa-1201-4259-a230923fds32") + .client_secret("CLDIE3F") + .redirect_uri("http://localhost:8888/redirect") + .grant_type("authorization_code") + .add_scope("Read.Write") + .add_scope("Fall.Down") + .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + + let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_client_id("bb301aaa-1201-4259-a230923fds32") + .with_client_secret("CLDIE3F") + .with_scopes(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .build(); + + let mut confidential_client = ConfidentialClient::try_from(credential).unwrap(); + + let oauth_uri = oauth + .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .unwrap(); + let credential_uri = confidential_client + .http_client + .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .unwrap(); + + assert_eq!(oauth_uri, credential_uri); + } + + #[test] + fn confidential_client_new() { + let mut oauth = OAuth::new(); + oauth + .client_id("bb301aaa-1201-4259-a230923fds32") + .client_secret("CLDIE3F") + .redirect_uri("http://localhost:8888/redirect") + .grant_type("authorization_code") + .add_scope("Read.Write") + .add_scope("Fall.Down") + .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + + let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_client_id("bb301aaa-1201-4259-a230923fds32") + .with_client_secret("CLDIE3F") + .with_scopes(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .build(); + + let mut confidential_client = + ConfidentialClient::new(credential, TokenCredentialOptions::default()).unwrap(); + let oauth_uri = oauth + .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .unwrap(); + let credential_uri = confidential_client + .http_client + .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .unwrap(); + + assert_eq!(oauth_uri, credential_uri); + } +} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs new file mode 100644 index 00000000..60458ca3 --- /dev/null +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -0,0 +1,9 @@ +mod authorization_code_credential; +mod confidential_client; +mod token_credential; +mod token_request; + +pub use authorization_code_credential::*; +pub use confidential_client::*; +pub use token_credential::*; +pub use token_request::*; diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs new file mode 100644 index 00000000..2057700f --- /dev/null +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -0,0 +1,6 @@ +use crate::identity::AzureAuthorityHost; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct TokenCredentialOptions { + pub(crate) azure_cloud_endpoint: AzureAuthorityHost, +} diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs new file mode 100644 index 00000000..833a37f2 --- /dev/null +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; +use graph_error::GraphResult; + +#[async_trait] +pub trait TokenRequest { + async fn get_token_silent(&self) -> GraphResult<reqwest::Response>; +} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs new file mode 100644 index 00000000..66823a10 --- /dev/null +++ b/graph-oauth/src/identity/mod.rs @@ -0,0 +1,5 @@ +mod authority; +mod credentials; + +pub use authority::*; +pub use credentials::*; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 59680819..35fdc11d 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -95,6 +95,8 @@ mod id_token; pub mod jwt; mod oauth_error; +pub mod identity; + pub mod oauth { pub use crate::access_token::AccessToken; pub use crate::auth::GrantSelector; From 03d51d9af7490a56c780712cdfd1718049c32f81 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 16 Apr 2023 04:27:11 -0400 Subject: [PATCH 002/118] Add token credential for authorization code and authorization code certificate --- examples/oauth/auth_code_grant.rs | 4 +- examples/oauth/auth_code_grant_pkce.rs | 4 +- examples/oauth/client_credentials.rs | 2 +- examples/oauth/code_flow.rs | 4 +- examples/oauth/implicit_grant.rs | 2 +- examples/oauth/open_id_connect.rs | 2 +- graph-error/src/authorization_failure.rs | 17 + graph-error/src/lib.rs | 3 + graph-oauth/Cargo.toml | 1 + graph-oauth/src/auth.rs | 153 +++++--- graph-oauth/src/discovery/graph_discovery.rs | 8 +- graph-oauth/src/grants.rs | 30 +- .../authorization_code_authorization_url.rs | 352 ++++++++++++++++++ ...thorization_code_certificate_credential.rs | 267 +++++++++++++ .../authorization_code_credential.rs | 294 ++++++++++++--- .../credentials/confidential_client.rs | 149 +++----- graph-oauth/src/identity/credentials/mod.rs | 8 + .../src/identity/credentials/prompt.rs | 29 ++ .../src/identity/credentials/response_mode.rs | 33 ++ .../identity/credentials/token_credential.rs | 2 +- .../src/identity/credentials/token_request.rs | 4 +- graph-oauth/src/identity/form_credential.rs | 7 + graph-oauth/src/identity/mod.rs | 3 + graph-oauth/src/identity/serialize.rs | 9 + graph-oauth/src/lib.rs | 4 +- src/lib.rs | 2 +- test-tools/src/oauth.rs | 10 +- tests/discovery_tests.rs | 18 +- tests/grants_authorization_code.rs | 10 +- tests/grants_code_flow.rs | 18 +- tests/grants_implicit.rs | 2 +- tests/grants_openid.rs | 2 +- tests/grants_token_flow.rs | 2 +- tests/oauth_tests.rs | 38 +- 34 files changed, 1217 insertions(+), 276 deletions(-) create mode 100644 graph-error/src/authorization_failure.rs create mode 100644 graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs create mode 100644 graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs create mode 100644 graph-oauth/src/identity/credentials/prompt.rs create mode 100644 graph-oauth/src/identity/credentials/response_mode.rs create mode 100644 graph-oauth/src/identity/form_credential.rs create mode 100644 graph-oauth/src/identity/serialize.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index a10eccac..4d593ee3 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -27,7 +27,7 @@ fn oauth_client() -> OAuth { .add_scope("files.readwrite.all") .add_scope("offline_access") .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") .response_type("code"); @@ -39,7 +39,7 @@ pub async fn set_and_req_access_code(access_code: AccessCode) -> GraphResult<()> // The response type is automatically set to token and the grant type is automatically // set to authorization_code if either of these were not previously set. // This is done here as an example. - oauth.access_code(access_code.code.as_str()); + oauth.authorization_code(access_code.code.as_str()); let mut request = oauth.build_async().authorization_code_grant(); // Returns reqwest::Response diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 632b5fca..8a5d5280 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -51,7 +51,7 @@ impl OAuthClient { .add_scope("user.read") .add_scope("user.readwrite") .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") .response_type("code"); @@ -80,7 +80,7 @@ async fn handle_redirect( // in case of an error here. let mut oauth = OAUTH_CLIENT.oauth(); - oauth.access_code(access_code.code.as_str()); + oauth.authorization_code(access_code.code.as_str()); let mut request = oauth.build_async().authorization_code_grant(); // Returns reqwest::Response diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index af6f834c..5cfe39ce 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -42,7 +42,7 @@ fn get_oauth_client() -> OAuth { .client_secret(CLIENT_SECRET) .add_scope("https://graph.microsoft.com/.default") .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.microsoftonline.com/common/adminconsent") + .authorization_url("https://login.microsoftonline.com/common/adminconsent") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token"); oauth } diff --git a/examples/oauth/code_flow.rs b/examples/oauth/code_flow.rs index 1519416a..4c124c77 100644 --- a/examples/oauth/code_flow.rs +++ b/examples/oauth/code_flow.rs @@ -62,7 +62,7 @@ fn oauth_web_client() -> OAuth { .add_scope("Files.ReadWrite.All") .add_scope("wl.offline_access") .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .access_token_url("https://login.live.com/oauth20_token.srf") .refresh_token_url("https://login.live.com/oauth20_token.srf") .response_mode("query") @@ -77,7 +77,7 @@ pub async fn set_and_req_access_code(access_code: AccessCode) { let mut oauth = oauth_web_client(); oauth.response_type("token"); oauth.state(access_code.state.as_str()); - oauth.access_code(access_code.code.as_str()); + oauth.authorization_code(access_code.code.as_str()); // Request the access token. let mut client = oauth.build_async().code_flow(); diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index c552ada5..d9cefa38 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -30,7 +30,7 @@ fn oauth_implicit_flow() -> OAuth { .response_type("token") .response_mode("query") .prompt("login") - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .access_token_url("https://login.live.com/oauth20_token.srf"); oauth } diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index 74c4c10a..eb89d9be 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -24,7 +24,7 @@ fn oauth_open_id() -> OAuth { oauth .client_id(CLIENT_ID) .client_secret(CLIENT_SECRET) - .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") .redirect_uri("http://localhost:8000/redirect") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs new file mode 100644 index 00000000..b120eae9 --- /dev/null +++ b/graph-error/src/authorization_failure.rs @@ -0,0 +1,17 @@ +#[derive(Debug, thiserror::Error)] +pub enum AuthorizationFailure { + #[error("Required value missing:\n{0:#?}", name)] + RequiredValue { + name: String, + message: Option<String>, + }, +} + +impl AuthorizationFailure { + pub fn required_value<T>(name: &str, message: Option<&str>) -> Result<T, AuthorizationFailure> { + Err(AuthorizationFailure::RequiredValue { + name: name.to_owned(), + message: message.map(|s| s.to_owned()), + }) + } +} diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index 5c8d1882..c47d3597 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -3,14 +3,17 @@ #[macro_use] extern crate serde; +mod authorization_failure; pub mod download; mod error; mod graph_failure; mod internal; pub mod io_error; +pub use authorization_failure::*; pub use error::*; pub use graph_failure::*; pub use internal::*; pub type GraphResult<T> = Result<T, GraphFailure>; +pub type AuthorizationResult<T> = Result<T, AuthorizationFailure>; diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 48717e35..d7b455c3 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["microsoft", "oauth", "authentication", "authorization"] categories = ["authentication", "web-programming::http-client"] [dependencies] +anyhow = "1.0.69" async-trait = "0.1.35" base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 5b406645..1eff20dd 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,11 +1,12 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; +use crate::identity::form_credential::FormCredential; use crate::identity::{Authority, AzureAuthorityHost}; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; -use graph_error::{GraphFailure, GraphResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use ring::rand::SecureRandom; use std::collections::btree_map::BTreeMap; use std::collections::{BTreeSet, HashMap}; @@ -22,11 +23,11 @@ use url::Url; pub enum OAuthCredential { ClientId, ClientSecret, - AuthorizeURL, - AccessTokenURL, - RefreshTokenURL, - RedirectURI, - AccessCode, + AuthorizationUrl, + AccessTokenUrl, + RefreshTokenUrl, + RedirectUri, + AuthorizationCode, AccessToken, RefreshToken, ResponseMode, @@ -58,11 +59,11 @@ impl OAuthCredential { match self { OAuthCredential::ClientId => "client_id", OAuthCredential::ClientSecret => "client_secret", - OAuthCredential::AuthorizeURL => "authorization_url", - OAuthCredential::AccessTokenURL => "access_token_url", - OAuthCredential::RefreshTokenURL => "refresh_token_url", - OAuthCredential::RedirectURI => "redirect_uri", - OAuthCredential::AccessCode => "code", + OAuthCredential::AuthorizationUrl => "authorization_url", + OAuthCredential::AccessTokenUrl => "access_token_url", + OAuthCredential::RefreshTokenUrl => "refresh_token_url", + OAuthCredential::RedirectUri => "redirect_uri", + OAuthCredential::AuthorizationCode => "code", OAuthCredential::AccessToken => "access_token", OAuthCredential::RefreshToken => "refresh_token", OAuthCredential::ResponseMode => "response_mode", @@ -101,7 +102,7 @@ impl OAuthCredential { | OAuthCredential::CodeVerifier | OAuthCredential::CodeChallenge | OAuthCredential::Password - | OAuthCredential::AccessCode + | OAuthCredential::AuthorizationCode ) } } @@ -171,17 +172,17 @@ impl OAuth { /// # use graph_oauth::oauth::OAuth; /// # use graph_oauth::oauth::OAuthCredential; /// # let mut oauth = OAuth::new(); - /// oauth.insert(OAuthCredential::AuthorizeURL, "https://example.com"); - /// assert!(oauth.contains(OAuthCredential::AuthorizeURL)); - /// println!("{:#?}", oauth.get(OAuthCredential::AuthorizeURL)); + /// oauth.insert(OAuthCredential::AuthorizationUrl, "https://example.com"); + /// assert!(oauth.contains(OAuthCredential::AuthorizationUrl)); + /// println!("{:#?}", oauth.get(OAuthCredential::AuthorizationUrl)); /// ``` pub fn insert<V: ToString>(&mut self, oac: OAuthCredential, value: V) -> &mut OAuth { let v = value.to_string(); match oac { - OAuthCredential::RefreshTokenURL + OAuthCredential::RefreshTokenUrl | OAuthCredential::PostLogoutRedirectURI - | OAuthCredential::AccessTokenURL - | OAuthCredential::AuthorizeURL + | OAuthCredential::AccessTokenUrl + | OAuthCredential::AuthorizationUrl | OAuthCredential::LogoutURL => { Url::parse(v.as_ref()).unwrap(); } @@ -201,16 +202,16 @@ impl OAuth { /// # use graph_oauth::oauth::OAuth; /// # use graph_oauth::oauth::OAuthCredential; /// # let mut oauth = OAuth::new(); - /// let entry = oauth.entry(OAuthCredential::AuthorizeURL, "https://example.com"); + /// let entry = oauth.entry(OAuthCredential::AuthorizationUrl, "https://example.com"); /// assert_eq!(entry, "https://example.com") /// ``` pub fn entry<V: ToString>(&mut self, oac: OAuthCredential, value: V) -> &mut String { let v = value.to_string(); match oac { - OAuthCredential::RefreshTokenURL + OAuthCredential::RefreshTokenUrl | OAuthCredential::PostLogoutRedirectURI - | OAuthCredential::AccessTokenURL - | OAuthCredential::AuthorizeURL + | OAuthCredential::AccessTokenUrl + | OAuthCredential::AuthorizationUrl | OAuthCredential::LogoutURL => { Url::parse(v.as_ref()).unwrap(); } @@ -229,7 +230,7 @@ impl OAuth { /// # use graph_oauth::oauth::OAuth; /// # use graph_oauth::oauth::OAuthCredential; /// # let mut oauth = OAuth::new(); - /// let a = oauth.get(OAuthCredential::AuthorizeURL); + /// let a = oauth.get(OAuthCredential::AuthorizationUrl); /// ``` pub fn get(&self, oac: OAuthCredential) -> Option<String> { self.credentials.get(oac.alias()).cloned() @@ -318,10 +319,10 @@ impl OAuth { /// ``` /// # use graph_oauth::oauth::OAuth; /// # let mut oauth = OAuth::new(); - /// oauth.authorize_url("https://example.com/authorize"); + /// oauth.authorization_url("https://example.com/authorize"); /// ``` - pub fn authorize_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AuthorizeURL, value) + pub fn authorization_url(&mut self, value: &str) -> &mut OAuth { + self.insert(OAuthCredential::AuthorizationUrl, value) } /// Set the access token url of a request for OAuth @@ -333,7 +334,7 @@ impl OAuth { /// oauth.access_token_url("https://example.com/token"); /// ``` pub fn access_token_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AccessTokenURL, value) + self.insert(OAuthCredential::AccessTokenUrl, value) } /// Set the refresh token url of a request for OAuth @@ -345,7 +346,7 @@ impl OAuth { /// oauth.refresh_token_url("https://example.com/token"); /// ``` pub fn refresh_token_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::RefreshTokenURL, value) + self.insert(OAuthCredential::RefreshTokenUrl, value) } /// Set the authorization, access token, and refresh token URL @@ -361,7 +362,7 @@ impl OAuth { let token_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/token",); let auth_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/authorize",); - self.authorize_url(&auth_url) + self.authorization_url(&auth_url) .access_token_url(&token_url) .refresh_token_url(&token_url) } @@ -383,7 +384,7 @@ impl OAuth { authority.as_ref() ); - self.authorize_url(&auth_url) + self.authorization_url(&auth_url) .access_token_url(&token_url) .refresh_token_url(&token_url) } @@ -397,7 +398,7 @@ impl OAuth { /// oauth.redirect_uri("https://localhost:8888/redirect"); /// ``` pub fn redirect_uri(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::RedirectURI, value) + self.insert(OAuthCredential::RedirectUri, value) } /// Set the access code. @@ -406,10 +407,10 @@ impl OAuth { /// ``` /// # use graph_oauth::oauth::OAuth; /// # let mut oauth = OAuth::new(); - /// oauth.access_code("LDSF[POK43"); + /// oauth.authorization_code("LDSF[POK43"); /// ``` - pub fn access_code(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AccessCode, value) + pub fn authorization_code(&mut self, value: &str) -> &mut OAuth { + self.insert(OAuthCredential::AuthorizationCode, value) } /// Set the response mode. @@ -471,9 +472,8 @@ impl OAuth { /// oauth.id_token(IdToken::new("1345", "code", "state", "session_state")); /// ``` pub fn id_token(&mut self, value: IdToken) -> &mut OAuth { - self.insert(OAuthCredential::IdToken, value.get_id_token().as_str()); if let Some(code) = value.get_code() { - self.access_code(code.as_str()); + self.authorization_code(code.as_str()); } if let Some(state) = value.get_state() { let _ = self.entry(OAuthCredential::State, state.as_str()); @@ -710,6 +710,10 @@ impl OAuth { self.insert(OAuthCredential::Password, value) } + pub fn refresh_token(&mut self, value: &str) -> &mut OAuth { + self.insert(OAuthCredential::RefreshToken, value) + } + /// Add a scope' for the OAuth URL. /// /// # Example @@ -843,6 +847,9 @@ impl OAuth { /// oauth.access_token(access_token); /// ``` pub fn access_token(&mut self, ac: AccessToken) { + if let Some(refresh_token) = ac.refresh_token() { + self.refresh_token(refresh_token.as_str()); + } self.access_token.replace(ac); } @@ -879,12 +886,16 @@ impl OAuth { /// println!("{:#?}", refresh_token); /// ``` pub fn get_refresh_token(&self) -> GraphResult<String> { + if let Some(refresh_token) = self.get(OAuthCredential::RefreshToken) { + return Ok(refresh_token); + } + match self.get_access_token() { Some(token) => match token.refresh_token() { Some(t) => Ok(t), None => OAuthError::error_from::<String>(OAuthCredential::RefreshToken), }, - None => OAuthError::error_from::<String>(OAuthCredential::AccessToken), + None => OAuthError::error_from::<String>(OAuthCredential::RefreshToken), } } @@ -926,7 +937,7 @@ impl OAuth { if let Some(redirect) = self.get(OAuthCredential::PostLogoutRedirectURI) { vec.push(redirect); - } else if let Some(redirect) = self.get(OAuthCredential::RedirectURI) { + } else if let Some(redirect) = self.get(OAuthCredential::RedirectUri) { vec.push(redirect); } webbrowser::open(vec.join("").as_str()).map_err(GraphFailure::from) @@ -950,7 +961,7 @@ impl OAuth { url.push_str("post_logout_redirect_uri="); url.push_str(redirect.as_str()); } else { - let redirect_uri = self.get_or_else(OAuthCredential::RedirectURI)?; + let redirect_uri = self.get_or_else(OAuthCredential::RedirectUri)?; url.push_str("post_logout_redirect_uri="); url.push_str(redirect_uri.as_str()); } @@ -959,11 +970,11 @@ impl OAuth { } impl OAuth { - fn get_or_else(&self, c: OAuthCredential) -> GraphResult<String> { + pub fn get_or_else(&self, c: OAuthCredential) -> GraphResult<String> { self.get(c).ok_or_else(|| OAuthError::credential_error(c)) } - fn form_encode_credentials( + pub fn form_encode_credentials( &mut self, pairs: Vec<OAuthCredential>, encoder: &mut Serializer<String>, @@ -984,7 +995,11 @@ impl OAuth { let mut map: HashMap<String, String> = HashMap::new(); for oac in pairs.iter() { if oac.eq(&OAuthCredential::RefreshToken) { - map.insert("refresh_token".into(), self.get_refresh_token()?); + if let Some(val) = self.get(*oac) { + map.insert(oac.to_string(), val); + } else { + map.insert("refresh_token".into(), self.get_refresh_token()?); + } } else if oac.alias().eq("scope") && !self.scopes.is_empty() { map.insert("scope".into(), self.join_scopes(" ")); } else if let Some(val) = self.get(*oac) { @@ -994,6 +1009,34 @@ impl OAuth { Ok(map) } + pub fn authorization_form( + &mut self, + form_credentials: Vec<FormCredential>, + ) -> AuthorizationResult<HashMap<String, String>> { + let mut map: HashMap<String, String> = HashMap::new(); + + for form_credential in form_credentials.iter() { + match form_credential { + FormCredential::Required(oac) => { + let val = self.get(*oac).ok_or(AuthorizationFailure::RequiredValue { + name: oac.alias().into(), + message: None, + })?; + map.insert(oac.to_string(), val); + } + FormCredential::NotRequired(oac) => { + if oac.eq(&OAuthCredential::Scopes) && !self.scopes.is_empty() { + map.insert("scope".into(), self.join_scopes(" ")); + } else if let Some(val) = self.get(*oac) { + map.insert(oac.to_string(), val); + } + } + } + } + + Ok(map) + } + pub fn encode_uri( &mut self, grant: GrantType, @@ -1006,7 +1049,7 @@ impl OAuth { GrantRequest::Authorization => { let _ = self.entry(OAuthCredential::ResponseType, "token"); self.form_encode_credentials(GrantType::TokenFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1028,7 +1071,7 @@ impl OAuth { let _ = self.entry(OAuthCredential::ResponseMode, "query"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1055,7 +1098,7 @@ impl OAuth { let _ = self.entry(OAuthCredential::ResponseType, "code"); let _ = self.entry(OAuthCredential::ResponseMode, "query"); self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1080,7 +1123,7 @@ impl OAuth { let _ = self.entry(OAuthCredential::ResponseType, "token"); } self.form_encode_credentials(GrantType::Implicit.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1100,7 +1143,7 @@ impl OAuth { GrantRequest::Authorization => { self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1124,7 +1167,7 @@ impl OAuth { match request_type { GrantRequest::Authorization => { self.form_encode_credentials(GrantType::ClientCredentials.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizeURL)?; + let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1679,7 +1722,7 @@ impl ImplicitGrant { .pre_request_check(self.grant, GrantRequest::Authorization); Ok(Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizeURL)? + .get_or_else(OAuthCredential::AuthorizationUrl)? .as_str(), )?) } @@ -1743,7 +1786,7 @@ impl AccessTokenGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizeURL)? + .get_or_else(OAuthCredential::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -1828,7 +1871,7 @@ impl AccessTokenGrant { pub fn access_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenURL); + let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -1861,7 +1904,7 @@ impl AccessTokenGrant { pub fn refresh_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenURL); + let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); @@ -1906,7 +1949,7 @@ impl AsyncAccessTokenGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizeURL)? + .get_or_else(OAuthCredential::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -1992,7 +2035,7 @@ impl AsyncAccessTokenGrant { pub fn access_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenURL); + let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2025,7 +2068,7 @@ impl AsyncAccessTokenGrant { pub fn refresh_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenURL); + let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); diff --git a/graph-oauth/src/discovery/graph_discovery.rs b/graph-oauth/src/discovery/graph_discovery.rs index 7a3f3724..6c16bc01 100644 --- a/graph-oauth/src/discovery/graph_discovery.rs +++ b/graph-oauth/src/discovery/graph_discovery.rs @@ -128,7 +128,7 @@ impl GraphDiscovery { GraphDiscovery::V1 => { let k: MicrosoftSigningKeysV1 = self.signing_keys()?; oauth - .authorize_url(k.authorization_endpoint.as_str()) + .authorization_url(k.authorization_endpoint.as_str()) .access_token_url(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); @@ -137,7 +137,7 @@ impl GraphDiscovery { GraphDiscovery::V2 | GraphDiscovery::Tenant(_) => { let k: MicrosoftSigningKeysV2 = self.signing_keys()?; oauth - .authorize_url(k.authorization_endpoint.as_str()) + .authorization_url(k.authorization_endpoint.as_str()) .access_token_url(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); @@ -162,7 +162,7 @@ impl GraphDiscovery { GraphDiscovery::V1 => { let k: MicrosoftSigningKeysV1 = self.async_signing_keys().await?; oauth - .authorize_url(k.authorization_endpoint.as_str()) + .authorization_url(k.authorization_endpoint.as_str()) .access_token_url(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); @@ -171,7 +171,7 @@ impl GraphDiscovery { GraphDiscovery::V2 | GraphDiscovery::Tenant(_) => { let k: MicrosoftSigningKeysV2 = self.async_signing_keys().await?; oauth - .authorize_url(k.authorization_endpoint.as_str()) + .authorization_url(k.authorization_endpoint.as_str()) .access_token_url(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); diff --git a/graph-oauth/src/grants.rs b/graph-oauth/src/grants.rs index 5abce89b..d25cdbd6 100644 --- a/graph-oauth/src/grants.rs +++ b/graph-oauth/src/grants.rs @@ -30,7 +30,7 @@ impl GrantType { | GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ OAuthCredential::ClientId, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::ResponseType, OAuthCredential::Scopes, ], @@ -38,7 +38,7 @@ impl GrantType { GrantType::CodeFlow => match grant_request { GrantRequest::Authorization => vec![ OAuthCredential::ClientId, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::State, OAuthCredential::ResponseType, OAuthCredential::Scopes, @@ -46,24 +46,24 @@ impl GrantType { GrantRequest::AccessToken => vec![ OAuthCredential::ClientId, OAuthCredential::ClientSecret, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::ResponseType, OAuthCredential::GrantType, - OAuthCredential::AccessCode, + OAuthCredential::AuthorizationCode, ], GrantRequest::RefreshToken => vec![ OAuthCredential::ClientId, OAuthCredential::ClientSecret, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::GrantType, - OAuthCredential::AccessCode, + OAuthCredential::AuthorizationCode, OAuthCredential::RefreshToken, ], }, GrantType::AuthorizationCode => match grant_request { GrantRequest::Authorization => vec![ OAuthCredential::ClientId, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::State, OAuthCredential::ResponseMode, OAuthCredential::ResponseType, @@ -77,8 +77,8 @@ impl GrantType { GrantRequest::AccessToken => vec![ OAuthCredential::ClientId, OAuthCredential::ClientSecret, - OAuthCredential::RedirectURI, - OAuthCredential::AccessCode, + OAuthCredential::RedirectUri, + OAuthCredential::AuthorizationCode, OAuthCredential::Scopes, OAuthCredential::GrantType, OAuthCredential::CodeVerifier, @@ -96,7 +96,7 @@ impl GrantType { | GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ OAuthCredential::ClientId, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::Scopes, OAuthCredential::ResponseType, OAuthCredential::ResponseMode, @@ -111,7 +111,7 @@ impl GrantType { GrantRequest::Authorization => vec![ OAuthCredential::ClientId, OAuthCredential::ResponseType, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::ResponseMode, OAuthCredential::Scopes, OAuthCredential::State, @@ -124,10 +124,10 @@ impl GrantType { GrantRequest::AccessToken => vec![ OAuthCredential::ClientId, OAuthCredential::ClientSecret, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::GrantType, OAuthCredential::Scopes, - OAuthCredential::AccessCode, + OAuthCredential::AuthorizationCode, OAuthCredential::CodeVerifier, ], GrantRequest::RefreshToken => vec![ @@ -141,7 +141,7 @@ impl GrantType { GrantType::ClientCredentials => match grant_request { GrantRequest::Authorization => vec![ OAuthCredential::ClientId, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::State, ], GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ @@ -163,7 +163,7 @@ impl GrantType { OAuthCredential::Username, OAuthCredential::Password, OAuthCredential::Scopes, - OAuthCredential::RedirectURI, + OAuthCredential::RedirectUri, OAuthCredential::ClientAssertion, ], }, diff --git a/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs b/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs new file mode 100644 index 00000000..836e63a5 --- /dev/null +++ b/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs @@ -0,0 +1,352 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::grants::GrantType; +use crate::identity::{Authority, AzureAuthorityHost, Prompt, ResponseMode}; +use crate::oauth::OAuthError; +use base64::Engine; +use ring::rand::SecureRandom; + +use graph_error::{GraphFailure, GraphResult}; + +use url::form_urlencoded::Serializer; +use url::Url; + +/// Get the authorization url required to perform the initial authorization and redirect in the +/// authorization code flow. +/// +/// The authorization code flow begins with the client directing the user to the /authorize +/// endpoint. +/// +/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application +/// to obtain authorized access to protected resources like web APIs. The auth code flow requires +/// a user-agent that supports redirection from the authorization server (the Microsoft identity platform) +/// back to your application. For example, a web browser, desktop, or mobile application operated +/// by a user to sign in to your app and access their data. +/// +/// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code +#[derive(Clone)] +pub struct AuthorizationCodeAuthorizationUrl { + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) redirect_uri: String, + pub(crate) authority: Authority, + pub(crate) response_mode: ResponseMode, + pub(crate) response_type: String, + pub(crate) nonce: Option<String>, + pub(crate) state: Option<String>, + pub(crate) scopes: Vec<String>, + pub(crate) prompt: Option<Prompt>, + pub(crate) domain_hint: Option<String>, + pub(crate) login_hint: Option<String>, + /// The code verifier is not included in the authorization URL. + /// You can set the code verifier here and then use the From trait + /// for [AuthorizationCodeCredential] which does use the code verifier. + pub(crate) code_verifier: Option<String>, + pub(crate) code_challenge: Option<String>, + pub(crate) code_challenge_method: Option<String>, +} + +impl AuthorizationCodeAuthorizationUrl { + pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthorizationCodeAuthorizationUrl { + AuthorizationCodeAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + authority: Authority::default(), + response_mode: ResponseMode::Query, + response_type: "code".to_owned(), + nonce: None, + state: None, + scopes: vec![], + prompt: None, + domain_hint: None, + login_hint: None, + code_verifier: None, + code_challenge: None, + code_challenge_method: None, + } + } + + pub fn grant_type(&self) -> GrantType { + GrantType::AuthorizationCode + } + + pub fn builder() -> AuthorizationCodeAuthorizationUrlBuilder { + AuthorizationCodeAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> GraphResult<Url> { + self.url_with_host(&AzureAuthorityHost::default()) + } + + pub fn url_with_host(&self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + let mut serializer = OAuth::new(); + + if self.redirect_uri.trim().is_empty() { + return OAuthError::error_from(OAuthCredential::RedirectUri); + } + + if self.client_id.trim().is_empty() { + return OAuthError::error_from(OAuthCredential::ClientId); + } + + serializer + .client_id(self.client_id.as_str()) + .redirect_uri(self.redirect_uri.as_str()) + .extend_scopes(self.scopes.clone()) + .authority(azure_authority_host, &self.authority) + .response_mode(self.response_mode.as_ref()) + .response_type(self.response_type.as_str()); + + if let Some(state) = self.state.as_ref() { + serializer.state(state.as_str()); + } + + if let Some(prompt) = self.prompt.as_ref() { + serializer.prompt(prompt.as_ref()); + } + + if let Some(domain_hint) = self.domain_hint.as_ref() { + serializer.domain_hint(domain_hint.as_str()); + } + + if let Some(login_hint) = self.login_hint.as_ref() { + serializer.login_hint(login_hint.as_str()); + } + + if let Some(code_challenge) = self.code_challenge.as_ref() { + serializer.code_challenge(code_challenge.as_str()); + } + + if let Some(code_challenge_method) = self.code_challenge_method.as_ref() { + serializer.code_challenge_method(code_challenge_method.as_str()); + } + + let authorization_credentials = vec![ + OAuthCredential::ClientId, + OAuthCredential::RedirectUri, + OAuthCredential::State, + OAuthCredential::ResponseMode, + OAuthCredential::ResponseType, + OAuthCredential::Scopes, + OAuthCredential::Prompt, + OAuthCredential::DomainHint, + OAuthCredential::LoginHint, + OAuthCredential::CodeChallenge, + OAuthCredential::CodeChallengeMethod, + ]; + + let mut encoder = Serializer::new(String::new()); + serializer.form_encode_credentials(authorization_credentials, &mut encoder); + + let mut url = Url::parse( + serializer + .get_or_else(OAuthCredential::AuthorizationUrl)? + .as_str(), + ) + .map_err(GraphFailure::from)?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } +} + +#[derive(Clone)] +pub struct AuthorizationCodeAuthorizationUrlBuilder { + authorization_code_authorize_url: AuthorizationCodeAuthorizationUrl, +} + +impl Default for AuthorizationCodeAuthorizationUrlBuilder { + fn default() -> Self { + Self::new() + } +} + +impl AuthorizationCodeAuthorizationUrlBuilder { + pub fn new() -> AuthorizationCodeAuthorizationUrlBuilder { + AuthorizationCodeAuthorizationUrlBuilder { + authorization_code_authorize_url: AuthorizationCodeAuthorizationUrl { + client_id: String::new(), + redirect_uri: String::new(), + authority: Authority::default(), + response_mode: ResponseMode::Query, + response_type: "code".to_owned(), + nonce: None, + state: None, + scopes: vec![], + prompt: None, + domain_hint: None, + login_hint: None, + code_verifier: None, + code_challenge: None, + code_challenge_method: None, + }, + } + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.authorization_code_authorize_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.authorization_code_authorize_url.client_id = client_id.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.authorization_code_authorize_url.authority = + Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.authorization_code_authorize_url.authority = authority.into(); + self + } + + /// Must include code for the authorization code flow. Can also include id_token or token + /// if using the hybrid flow. Default is code. + pub fn with_response_type<T: AsRef<str>>(&mut self, response_type: T) -> &mut Self { + self.authorization_code_authorize_url.response_type = response_type.as_ref().to_owned(); + self + } + + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - **query**: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter is not supported when requesting an + /// ID token by using the implicit flow. + /// - **fragment**: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - **form_post**: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. + pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { + self.authorization_code_authorize_url.response_mode = response_mode; + self + } + + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { + self.authorization_code_authorize_url.nonce = Some(nonce.as_ref().to_owned()); + self + } + + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { + self.authorization_code_authorize_url.state = Some(state.as_ref().to_owned()); + self + } + + pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.authorization_code_authorize_url.scopes = + scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + /// Indicates the type of user interaction that is required. Valid values are login, none, + /// consent, and select_account. + /// + /// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on. + /// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt. + /// If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error. + /// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to + /// grant permissions to the app. + /// - **prompt=select_account** interrupts single sign-on providing account selection experience + /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. + pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { + self.authorization_code_authorize_url.prompt = Some(prompt); + self + } + + pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { + self.authorization_code_authorize_url.domain_hint = Some(domain_hint.as_ref().to_owned()); + self + } + + pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { + self.authorization_code_authorize_url.login_hint = Some(login_hint.as_ref().to_owned()); + self + } + + pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.authorization_code_authorize_url.code_verifier = + Some(code_verifier.as_ref().to_owned()); + self + } + + /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). + /// Required if code_challenge_method is included. + pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { + self.authorization_code_authorize_url.code_challenge = + Some(code_challenge.as_ref().to_owned()); + self + } + + /// The method used to encode the code_verifier for the code_challenge parameter. + /// This SHOULD be S256, but the spec allows the use of plain if the client can't support SHA256. + /// + /// If excluded, code_challenge is assumed to be plaintext if code_challenge is included. + /// The Microsoft identity platform supports both plain and S256. + pub fn with_code_challenge_method<T: AsRef<str>>( + &mut self, + code_challenge_method: T, + ) -> &mut Self { + self.authorization_code_authorize_url.code_challenge_method = + Some(code_challenge_method.as_ref().to_owned()); + self + } + + /// Generate a code challenge and code verifier for the + /// authorization code grant flow using proof key for + /// code exchange (PKCE) and SHA256. + /// + /// This method automatically sets the code_verifier, + /// code_challenge, and code_challenge_method fields. + /// + /// For authorization, the code_challenge_method parameter in the request body + /// is automatically set to 'S256'. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + pub fn generate_sha256_challenge_and_verifier(&mut self) -> Result<(), GraphFailure> { + let mut buf = [0; 32]; + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf)?; + let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf); + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(verifier.as_bytes()); + let code_challenge = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + + self.with_code_verifier(verifier); + self.with_code_challenge(code_challenge); + self.with_code_challenge_method("S256"); + Ok(()) + } + + pub fn build(&self) -> AuthorizationCodeAuthorizationUrl { + self.authorization_code_authorize_url.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn serialize_uri() { + let authorizer = AuthorizationCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scopes(["read", "write"]) + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs new file mode 100644 index 00000000..20d77e51 --- /dev/null +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -0,0 +1,267 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::grants::GrantType; +use crate::identity::form_credential::FormCredential; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use std::collections::HashMap; + +use url::Url; + +#[derive(Clone)] +pub struct AuthorizationCodeCertificateCredential { + /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. + pub(crate) authorization_code: Option<String>, + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) client_secret: String, + pub(crate) redirect_uri: String, + pub(crate) code_verifier: Option<String>, + pub(crate) client_assertion_type: String, + pub(crate) client_assertion: String, + pub(crate) scopes: Vec<String>, + /// The Azure Active Directory tenant (directory) Id of the service principal. + pub(crate) authority: Authority, + serializer: OAuth, +} + +impl AuthorizationCodeCertificateCredential { + pub fn new<T: AsRef<str>>( + client_id: T, + client_secret: T, + authorization_code: T, + redirect_uri: T, + client_assertion: T, + ) -> AuthorizationCodeCertificateCredential { + AuthorizationCodeCertificateCredential { + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + code_verifier: None, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + .to_owned(), + client_assertion: client_assertion.as_ref().to_owned(), + scopes: vec![], + authority: Default::default(), + serializer: OAuth::new(), + } + } + + pub fn grant_type(&self) -> GrantType { + GrantType::AuthorizationCode + } + + pub fn builder() -> AuthorizationCodeCertificateCredentialBuilder { + AuthorizationCodeCertificateCredentialBuilder::new() + } +} + +impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + let uri = self + .serializer + .get_or_else(OAuthCredential::AccessTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.authorization_code.is_some() && self.refresh_token.is_some() { + return AuthorizationFailure::required_value( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Authorization code and refresh token cannot be set at the same time - choose one or the other"), + ); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + } + + if self.client_secret.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::ClientSecret.alias(), + None, + ); + } + + if self.client_assertion.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::ClientAssertion.alias(), + None, + ); + } + + if self.client_assertion_type.trim().is_empty() { + self.client_assertion_type = + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_owned(); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .redirect_uri(self.redirect_uri.as_str()) + .client_assertion(self.client_assertion.as_str()) + .client_assertion_type(self.client_assertion_type.as_str()) + .extend_scopes(self.scopes.clone()); + + if let Some(code_verifier) = self.code_verifier.as_ref() { + self.serializer.code_verifier(code_verifier.as_ref()); + } + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::RefreshToken.alias(), + None, + ); + } + + self.serializer + .refresh_token(refresh_token.as_ref()) + .grant_type("refresh_token"); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::RefreshToken), + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::Required(OAuthCredential::ClientAssertion), + FormCredential::Required(OAuthCredential::ClientAssertionType), + ]); + } else if let Some(authorization_code) = self.authorization_code.as_ref() { + if authorization_code.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::AuthorizationCode.alias(), + None, + ); + } + + self.serializer + .authorization_code(authorization_code.as_str()) + .grant_type("authorization_code"); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::AuthorizationCode), + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::CodeVerifier), + FormCredential::Required(OAuthCredential::ClientAssertion), + FormCredential::Required(OAuthCredential::ClientAssertionType), + ]); + } + + AuthorizationFailure::required_value( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Either authorization code or refresh token is required"), + ) + } +} + +#[derive(Clone)] +pub struct AuthorizationCodeCertificateCredentialBuilder { + authorization_code_credential: AuthorizationCodeCertificateCredential, +} + +impl AuthorizationCodeCertificateCredentialBuilder { + fn new() -> AuthorizationCodeCertificateCredentialBuilder { + Self { + authorization_code_credential: AuthorizationCodeCertificateCredential { + authorization_code: None, + refresh_token: None, + client_id: String::new(), + client_secret: String::new(), + redirect_uri: String::new(), + code_verifier: None, + client_assertion_type: String::new(), + client_assertion: String::new(), + scopes: vec![], + authority: Default::default(), + serializer: OAuth::new(), + }, + } + } + + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.authorization_code_credential.authorization_code = + Some(authorization_code.as_ref().to_owned()); + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.authorization_code_credential.authorization_code = None; + self.authorization_code_credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.authorization_code_credential.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.authorization_code_credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.authorization_code_credential.client_secret = client_secret.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.authorization_code_credential.authority = + Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.authorization_code_credential.authority = authority.into(); + self + } + + pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.authorization_code_credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + self + } + + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + self.authorization_code_credential.client_assertion = client_assertion.as_ref().to_owned(); + self + } + + pub fn with_client_assertion_type<T: AsRef<str>>( + &mut self, + client_assertion_type: T, + ) -> &mut Self { + self.authorization_code_credential.client_assertion_type = + client_assertion_type.as_ref().to_owned(); + self + } + + pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.authorization_code_credential.scopes = + scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn build(&self) -> AuthorizationCodeCertificateCredential { + self.authorization_code_credential.clone() + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index e01b145d..506a5e89 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,42 +1,68 @@ +use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; -use crate::identity::Authority; +use crate::identity::form_credential::FormCredential; +use crate::identity::{ + Authority, AuthorizationCodeAuthorizationUrl, AuthorizationSerializer, AzureAuthorityHost, +}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use std::collections::HashMap; -/// Creates an instance of the ClientSecretCredential with the details needed to authenticate -/// against Azure Active Directory with a prefetched authorization code. -/// -/// <param name="clientSecret">A client secret that was generated for the App Registration used to authenticate the client.</param> -/// <param name="authorizationCode">The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. -/// See https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow for more information.</param> +use url::Url; #[derive(Clone)] pub struct AuthorizationCodeCredential { /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. - pub(crate) authorization_code: String, + pub(crate) authorization_code: Option<String>, + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, /// The client (application) ID of the service principal pub(crate) client_id: String, pub(crate) client_secret: String, pub(crate) redirect_uri: String, pub(crate) scopes: Vec<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) tenant_id: Authority, + pub(crate) authority: Authority, pub(crate) code_verifier: Option<String>, + serializer: OAuth, } +/* + pub(crate) client_id: String, + pub(crate) redirect_uri: String, + pub(crate) authority: Authority, + pub(crate) response_mode: ResponseMode, + pub(crate) response_type: String, + pub(crate) nonce: Option<String>, + pub(crate) state: Option<String>, + pub(crate) scopes: Vec<String>, + pub(crate) prompt: Option<Prompt>, + pub(crate) domain_hint: Option<String>, + pub(crate) login_hint: Option<String>, + pub(crate) code_challenge: Option<String>, + pub(crate) code_challenge_method: Option<String>, + + authority + +*/ + impl AuthorizationCodeCredential { - pub fn new( - client_id: &str, - client_secret: &str, - authorization_code: &str, - redirect_uri: &str, + pub fn new<T: AsRef<str>>( + client_id: T, + client_secret: T, + authorization_code: T, + redirect_uri: T, ) -> AuthorizationCodeCredential { AuthorizationCodeCredential { - authorization_code: authorization_code.to_owned(), - client_id: client_id.to_owned(), - client_secret: client_secret.to_owned(), - redirect_uri: redirect_uri.to_owned(), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), scopes: vec![], - tenant_id: Default::default(), + authority: Default::default(), code_verifier: None, + serializer: OAuth::new(), } } @@ -44,65 +70,192 @@ impl AuthorizationCodeCredential { GrantType::AuthorizationCode } - pub fn builder(authorization_code: &str) -> AuthorizationCodeCredentialBuilder { - let credential_builder = AuthorizationCodeCredentialBuilder::create(authorization_code); - credential_builder + pub fn builder() -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new() } } +impl AuthorizationSerializer for AuthorizationCodeCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + let uri = self + .serializer + .get_or_else(OAuthCredential::AccessTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.authorization_code.is_some() && self.refresh_token.is_some() { + return AuthorizationFailure::required_value( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Authorization code and refresh token cannot be set at the same time - choose one or the other"), + ); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + } + + if self.client_secret.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::ClientSecret.alias(), + None, + ); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .extend_scopes(self.scopes.clone()); + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::RefreshToken.alias(), + Some("Either authorization code or refresh token is required"), + ); + } + + self.serializer + .grant_type("refresh_token") + .refresh_token(refresh_token.as_ref()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ClientSecret), + FormCredential::Required(OAuthCredential::RefreshToken), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scopes), + ]); + } else if let Some(authorization_code) = self.authorization_code.as_ref() { + if authorization_code.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::RefreshToken.alias(), + Some("Either authorization code or refresh token is required"), + ); + } + + self.serializer + .authorization_code(authorization_code.as_ref()) + .grant_type("authorization_code") + .redirect_uri(self.redirect_uri.as_str()); + + if let Some(code_verifier) = self.code_verifier.as_ref() { + self.serializer.code_verifier(code_verifier.as_str()); + } + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ClientSecret), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::AuthorizationCode), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::CodeVerifier), + ]); + } + + AuthorizationFailure::required_value( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Either authorization code or refresh token is required"), + ) + } +} + +#[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { authorization_code_credential: AuthorizationCodeCredential, } impl AuthorizationCodeCredentialBuilder { - pub fn create(authorization_code: &str) -> AuthorizationCodeCredentialBuilder { + fn new() -> AuthorizationCodeCredentialBuilder { Self { authorization_code_credential: AuthorizationCodeCredential { - authorization_code: authorization_code.to_owned(), - client_id: Default::default(), - client_secret: Default::default(), - redirect_uri: Default::default(), + authorization_code: None, + refresh_token: None, + client_id: String::new(), + client_secret: String::new(), + redirect_uri: String::new(), scopes: vec![], - tenant_id: Default::default(), + authority: Default::default(), code_verifier: None, + serializer: OAuth::new(), }, } } - pub fn with_redirect_uri( + pub fn with_authorization_code<T: AsRef<str>>( + &mut self, + authorization_code: T, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.authorization_code = + Some(authorization_code.as_ref().to_owned()); + self + } + + pub fn with_refresh_token<T: AsRef<str>>( + &mut self, + refresh_token: T, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.authorization_code = None; + self.authorization_code_credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>( &mut self, - redirect_uri: &str, + redirect_uri: T, ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.redirect_uri = redirect_uri.to_owned(); + self.authorization_code_credential.redirect_uri = redirect_uri.as_ref().to_owned(); self } - pub fn with_client_id(&mut self, client_id: &str) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.client_id = client_id.to_owned(); + pub fn with_client_id<T: AsRef<str>>( + &mut self, + client_id: T, + ) -> &mut AuthorizationCodeCredentialBuilder { + self.authorization_code_credential.client_id = client_id.as_ref().to_owned(); self } - pub fn with_client_secret( + pub fn with_client_secret<T: AsRef<str>>( &mut self, - client_secret: &str, + client_secret: T, ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.client_secret = client_secret.to_owned(); + self.authorization_code_credential.client_secret = client_secret.as_ref().to_owned(); self } - pub fn with_tenant_id<T: Into<Authority>>( + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.authorization_code_credential.authority = + Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>( &mut self, - tenant_id: T, + authority: T, ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.tenant_id = tenant_id.into(); + self.authorization_code_credential.authority = authority.into(); self } - pub fn with_code_verifier( + pub fn with_code_verifier<T: AsRef<str>>( &mut self, - code_verifier: &str, + code_verifier: T, ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.code_verifier = Some(code_verifier.to_owned()); + self.authorization_code_credential.code_verifier = Some(code_verifier.as_ref().to_owned()); self } @@ -115,31 +268,72 @@ impl AuthorizationCodeCredentialBuilder { self } - pub fn build(&mut self) -> AuthorizationCodeCredential { + pub fn build(&self) -> AuthorizationCodeCredential { self.authorization_code_credential.clone() } } +impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { + fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { + let mut builder = AuthorizationCodeCredentialBuilder::new(); + builder + .with_scopes(value.scopes) + .with_client_id(value.client_id) + .with_redirect_uri(value.redirect_uri) + .with_authority(value.authority); + + if let Some(code_verifier) = value.code_verifier.as_ref() { + builder.with_code_verifier(code_verifier); + } + + builder + } +} + #[cfg(test)] mod test { use super::*; - use url::Url; #[test] fn with_tenant_id_common() { - let credential = AuthorizationCodeCredential::builder("") - .with_tenant_id(Authority::TenantId("common".into())) + let credential = AuthorizationCodeCredential::builder() + .with_authority(Authority::TenantId("common".into())) .build(); - assert_eq!(credential.tenant_id, Authority::TenantId("common".into())) + assert_eq!(credential.authority, Authority::TenantId("common".into())) } #[test] fn with_tenant_id_adfs() { - let credential = AuthorizationCodeCredential::builder("") - .with_tenant_id(Authority::AzureDirectoryFederatedServices) + let credential = AuthorizationCodeCredential::builder() + .with_authority(Authority::AzureDirectoryFederatedServices) .build(); - assert_eq!(credential.tenant_id.as_ref(), "adfs"); + assert_eq!(credential.authority.as_ref(), "adfs"); + } + + #[test] + #[should_panic] + fn authorization_code_missing_required_value() { + let mut credential_builder = AuthorizationCodeCredential::builder(); + credential_builder + .with_redirect_uri("https://localhost:8080") + .with_client_id("client_id") + .with_client_secret("client_secret") + .with_scopes(vec!["scope"]) + .with_tenant("tenant_id"); + let mut credential = credential_builder.build(); + let _ = credential.form().unwrap(); + } + + #[test] + #[should_panic] + fn required_value_missing_client_id() { + let mut credential_builder = AuthorizationCodeCredential::builder(); + credential_builder + .with_authorization_code("code") + .with_refresh_token("token"); + let mut credential = credential_builder.build(); + let _ = credential.form().unwrap(); } } diff --git a/graph-oauth/src/identity/credentials/confidential_client.rs b/graph-oauth/src/identity/credentials/confidential_client.rs index 8dec6f05..fac975e6 100644 --- a/graph-oauth/src/identity/credentials/confidential_client.rs +++ b/graph-oauth/src/identity/credentials/confidential_client.rs @@ -1,141 +1,116 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::credentials::{AuthorizationCodeCredential, TokenCredentialOptions}; -use crate::identity::TokenRequest; -use crate::oauth::OAuthError; +use crate::identity::{ + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, + TokenCredentialOptions, TokenRequest, +}; use async_trait::async_trait; -use graph_error::{GraphFailure, GraphResult}; +use graph_error::GraphResult; use reqwest::Response; -#[derive(Default)] pub struct ConfidentialClient { - http_client: OAuth, + http_client: reqwest::Client, + credential: Box<dyn AuthorizationSerializer + Send>, token_credential_options: TokenCredentialOptions, } impl ConfidentialClient { pub fn new<T>(credential: T, options: TokenCredentialOptions) -> GraphResult<ConfidentialClient> where - T: TryInto<ConfidentialClient, Error = GraphFailure>, + T: Into<ConfidentialClient>, { - let mut cred = credential.try_into()?; - cred.token_credential_options = options; - Ok(cred) + let mut confidential_client = credential.into(); + confidential_client.token_credential_options = options; + Ok(confidential_client) } } #[async_trait] impl TokenRequest for ConfidentialClient { - async fn get_token_silent(&self) -> GraphResult<Response> { - // self.http_client.build_async().authorization_code_grant(). + fn get_token_silent(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let uri = self + .credential + .uri(&self.token_credential_options.azure_authority_host)?; + let form = self.credential.form()?; + let http_client = reqwest::blocking::Client::new(); + Ok(http_client.post(uri).form(&form).send()?) } -} - -impl TryFrom<AuthorizationCodeCredential> for ConfidentialClient { - type Error = GraphFailure; - - fn try_from(value: AuthorizationCodeCredential) -> Result<Self, Self::Error> { - let mut client = ConfidentialClient::default(); - if value.authorization_code.trim().is_empty() { - return OAuthError::error_from(OAuthCredential::AuthorizeURL); - } - - if value.client_id.trim().is_empty() { - return OAuthError::error_from(OAuthCredential::ClientId); - } + async fn get_token_silent_async(&mut self) -> anyhow::Result<Response> { + let uri = self + .credential + .uri(&self.token_credential_options.azure_authority_host)?; + let form = self.credential.form()?; + Ok(self.http_client.post(uri).form(&form).send().await?) + } +} - if value.client_secret.trim().is_empty() { - return OAuthError::error_from(OAuthCredential::ClientSecret); +impl From<AuthorizationCodeCredential> for ConfidentialClient { + fn from(value: AuthorizationCodeCredential) -> Self { + ConfidentialClient { + http_client: reqwest::Client::new(), + credential: Box::new(value), + token_credential_options: Default::default(), } + } +} - client - .http_client - .access_code(value.authorization_code.as_str()) - .client_id(value.client_id.as_str()) - .client_secret(value.client_secret.as_str()) - .redirect_uri(value.redirect_uri.as_str()) - .extend_scopes(value.scopes) - .authority( - &client.token_credential_options.azure_cloud_endpoint, - &value.tenant_id, - ); - - if let Some(code_verifier) = value.code_verifier.as_ref() { - client.http_client.code_verifier(code_verifier.as_str()); +impl From<AuthorizationCodeCertificateCredential> for ConfidentialClient { + fn from(value: AuthorizationCodeCertificateCredential) -> Self { + ConfidentialClient { + http_client: reqwest::Client::new(), + credential: Box::new(value), + token_credential_options: Default::default(), } - - Ok(client) } } #[cfg(test)] mod test { use super::*; - use crate::grants::{GrantRequest, GrantType}; - use crate::identity::credentials::AuthorizationCodeCredentialBuilder; - use url::Url; + use crate::identity::{Authority, AzureAuthorityHost}; #[test] - fn test_auth_code_grant_serialization() { - let mut oauth = OAuth::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .client_secret("CLDIE3F") - .redirect_uri("http://localhost:8888/redirect") - .grant_type("authorization_code") - .add_scope("Read.Write") - .add_scope("Fall.Down") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - - let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + fn confidential_client_new() { + let credential = AuthorizationCodeCredential::builder() + .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") .with_scopes(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") .build(); - let mut confidential_client = ConfidentialClient::try_from(credential).unwrap(); - - let oauth_uri = oauth - .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) - .unwrap(); + let mut confidential_client = + ConfidentialClient::new(credential, TokenCredentialOptions::default()).unwrap(); let credential_uri = confidential_client - .http_client - .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .credential + .uri(&AzureAuthorityHost::AzurePublic) .unwrap(); - assert_eq!(oauth_uri, credential_uri); + assert_eq!( + "https://login.microsoftonline.com/common/oauth2/v2.0/token", + credential_uri.as_str() + ); } #[test] - fn confidential_client_new() { - let mut oauth = OAuth::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .client_secret("CLDIE3F") - .redirect_uri("http://localhost:8888/redirect") - .grant_type("authorization_code") - .add_scope("Read.Write") - .add_scope("Fall.Down") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - - let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + fn confidential_client_tenant() { + let credential = AuthorizationCodeCredential::builder() + .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") .with_scopes(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") + .with_authority(Authority::Consumers) .build(); - let mut confidential_client = ConfidentialClient::new(credential, TokenCredentialOptions::default()).unwrap(); - let oauth_uri = oauth - .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) - .unwrap(); let credential_uri = confidential_client - .http_client - .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) + .credential + .uri(&AzureAuthorityHost::AzurePublic) .unwrap(); - assert_eq!(oauth_uri, credential_uri); + assert_eq!( + "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", + credential_uri.as_str() + ); } } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 60458ca3..8250e5af 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,9 +1,17 @@ +mod authorization_code_authorization_url; +mod authorization_code_certificate_credential; mod authorization_code_credential; mod confidential_client; +mod prompt; +mod response_mode; mod token_credential; mod token_request; +pub use authorization_code_authorization_url::*; +pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use confidential_client::*; +pub use prompt::*; +pub use response_mode::*; pub use token_credential::*; pub use token_request::*; diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs new file mode 100644 index 00000000..e7680ccc --- /dev/null +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -0,0 +1,29 @@ +/// Indicates the type of user interaction that is required. Valid values are login, none, +/// consent, and select_account. +/// +/// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on. +/// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt. +/// If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error. +/// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to +/// grant permissions to the app. +/// - **prompt=select_account** interrupts single sign-on providing account selection experience +/// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Prompt { + #[default] + None, + Login, + Consent, + SelectAccount, +} + +impl AsRef<str> for Prompt { + fn as_ref(&self) -> &'static str { + match self { + Prompt::None => "none", + Prompt::Login => "login", + Prompt::Consent => "consent", + Prompt::SelectAccount => "select_account", + } + } +} diff --git a/graph-oauth/src/identity/credentials/response_mode.rs b/graph-oauth/src/identity/credentials/response_mode.rs new file mode 100644 index 00000000..1c8315e3 --- /dev/null +++ b/graph-oauth/src/identity/credentials/response_mode.rs @@ -0,0 +1,33 @@ +/// Specifies how the identity platform should return the requested token to your app. +/// +/// Supported values: +/// +/// - **query**: Default when requesting an access token. Provides the code as a query string +/// parameter on your redirect URI. The query parameter is not supported when requesting an +/// ID token by using the implicit flow. +/// - fragment: Default when requesting an ID token by using the implicit flow. +/// Also supported if requesting only a code. +/// - form_post: Executes a POST containing the code to your redirect URI. +/// Supported when requesting a code. +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ResponseMode { + /// Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter is not supported when requesting an + /// ID token by using the implicit flow. + #[default] + Query, + /// Default when requesting an ID token by using the implicit flow. Also supported if requesting only a code. + Fragment, + /// Executes a POST containing the code to your redirect URI. Supported when requesting a code. + FormPost, +} + +impl AsRef<str> for ResponseMode { + fn as_ref(&self) -> &'static str { + match self { + ResponseMode::Query => "query", + ResponseMode::Fragment => "fragment", + ResponseMode::FormPost => "form_post", + } + } +} diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 2057700f..2b2b0cfc 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -2,5 +2,5 @@ use crate::identity::AzureAuthorityHost; #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct TokenCredentialOptions { - pub(crate) azure_cloud_endpoint: AzureAuthorityHost, + pub(crate) azure_authority_host: AzureAuthorityHost, } diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 833a37f2..799979e9 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; -use graph_error::GraphResult; #[async_trait] pub trait TokenRequest { - async fn get_token_silent(&self) -> GraphResult<reqwest::Response>; + fn get_token_silent(&mut self) -> anyhow::Result<reqwest::blocking::Response>; + async fn get_token_silent_async(&mut self) -> anyhow::Result<reqwest::Response>; } diff --git a/graph-oauth/src/identity/form_credential.rs b/graph-oauth/src/identity/form_credential.rs new file mode 100644 index 00000000..75a943a1 --- /dev/null +++ b/graph-oauth/src/identity/form_credential.rs @@ -0,0 +1,7 @@ +use crate::auth::OAuthCredential; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum FormCredential { + Required(OAuthCredential), + NotRequired(OAuthCredential), +} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 66823a10..7e09eb62 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,5 +1,8 @@ mod authority; mod credentials; +pub(crate) mod form_credential; +mod serialize; pub use authority::*; pub use credentials::*; +pub use serialize::*; diff --git a/graph-oauth/src/identity/serialize.rs b/graph-oauth/src/identity/serialize.rs new file mode 100644 index 00000000..ab964910 --- /dev/null +++ b/graph-oauth/src/identity/serialize.rs @@ -0,0 +1,9 @@ +use crate::identity::AzureAuthorityHost; +use graph_error::{AuthorizationResult, GraphResult}; +use std::collections::HashMap; +use url::Url; + +pub trait AuthorizationSerializer { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url>; + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>>; +} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 35fdc11d..7ad890e4 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -34,7 +34,7 @@ //! .add_scope("files.readwrite.all") //! .add_scope("offline_access") //! .redirect_uri("http://localhost:8000/redirect") -//! .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") +//! .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") //! .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .response_type("code") @@ -55,7 +55,7 @@ //! ``` //! # use graph_oauth::oauth::OAuth; //! # let mut oauth = OAuth::new(); -//! oauth.access_code("<ACCESS CODE>"); +//! oauth.authorization_code("<ACCESS CODE>"); //! ``` //! //! Perform an authorization code grant request for an access token: diff --git a/src/lib.rs b/src/lib.rs index ed762d07..690637dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,7 +190,7 @@ //! .add_scope("files.readwrite.all") //! .add_scope("offline_access") //! .redirect_uri("http://localhost:8000/redirect") -//! .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") +//! .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") //! .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .response_type("code") diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs index 7507d51f..c990ddd9 100644 --- a/test-tools/src/oauth.rs +++ b/test-tools/src/oauth.rs @@ -8,9 +8,9 @@ pub struct OAuthTestTool; impl OAuthTestTool { fn match_grant_credential(grant_request: GrantRequest) -> OAuthCredential { match grant_request { - GrantRequest::Authorization => OAuthCredential::AuthorizeURL, - GrantRequest::AccessToken => OAuthCredential::AccessTokenURL, - GrantRequest::RefreshToken => OAuthCredential::RefreshTokenURL, + GrantRequest::Authorization => OAuthCredential::AuthorizationUrl, + GrantRequest::AccessToken => OAuthCredential::AccessTokenUrl, + GrantRequest::RefreshToken => OAuthCredential::RefreshTokenUrl, } } @@ -22,13 +22,13 @@ impl OAuthTestTool { ) { let mut url = String::new(); if grant_request.eq(&GrantRequest::AccessToken) { - let mut atu = oauth.get(OAuthCredential::AccessTokenURL).unwrap(); + let mut atu = oauth.get(OAuthCredential::AccessTokenUrl).unwrap(); if !atu.ends_with('?') { atu.push('?'); } url.push_str(atu.as_str()); } else if grant_request.eq(&GrantRequest::RefreshToken) { - let mut rtu = oauth.get(OAuthCredential::RefreshTokenURL).unwrap(); + let mut rtu = oauth.get(OAuthCredential::RefreshTokenUrl).unwrap(); if !rtu.ends_with('?') { rtu.push('?'); } diff --git a/tests/discovery_tests.rs b/tests/discovery_tests.rs index 8ae735bf..8aa7b986 100644 --- a/tests/discovery_tests.rs +++ b/tests/discovery_tests.rs @@ -9,15 +9,15 @@ fn graph_discovery_oauth_v1() { let oauth: OAuth = GraphDiscovery::V1.oauth().unwrap(); let keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.signing_keys().unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizeURL), + oauth.get(OAuthCredential::AuthorizationUrl), Some(keys.authorization_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenURL), + oauth.get(OAuthCredential::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenURL), + oauth.get(OAuthCredential::RefreshTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( @@ -31,15 +31,15 @@ fn graph_discovery_oauth_v2() { let oauth: OAuth = GraphDiscovery::V2.oauth().unwrap(); let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.signing_keys().unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizeURL), + oauth.get(OAuthCredential::AuthorizationUrl), Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenURL), + oauth.get(OAuthCredential::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenURL), + oauth.get(OAuthCredential::RefreshTokenUrl), Some(keys.token_endpoint) ); assert_eq!( @@ -53,15 +53,15 @@ async fn async_graph_discovery_oauth_v2() { let oauth: OAuth = GraphDiscovery::V2.async_oauth().await.unwrap(); let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.async_signing_keys().await.unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizeURL), + oauth.get(OAuthCredential::AuthorizationUrl), Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenURL), + oauth.get(OAuthCredential::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenURL), + oauth.get(OAuthCredential::RefreshTokenUrl), Some(keys.token_endpoint) ); assert_eq!( diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index c51d80c3..d25c1ed0 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -7,7 +7,7 @@ use url::{Host, Url}; pub fn authorization_url() { let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.microsoftonline.com/common/oauth2/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .response_type("code") .redirect_uri("http://localhost:8080") @@ -45,8 +45,8 @@ fn access_token_uri() { .grant_type("authorization_code") .add_scope("Read.Write") .add_scope("Fall.Down") - .access_code("11201a230923f-4259-a230011201a230923f") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .authorization_code("11201a230923f-4259-a230011201a230923f") + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .code_verifier("bb301aaab3011201a230923f-4259-a230923fds32"); let test_url = "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL&scope=Fall.Down+Read.Write&grant_type=authorization_code&code_verifier=bb301aaab3011201a230923f-4259-a230923fds32"; @@ -66,7 +66,7 @@ fn refresh_token_uri() { .grant_type("refresh_token") .add_scope("Read.Write") .add_scope("Fall.Down") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); let mut access_token = AccessToken::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); @@ -84,7 +84,7 @@ fn refresh_token_uri() { pub fn access_token_body_contains() { let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.microsoftonline.com/common/oauth2/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .redirect_uri("http://localhost:8080") .add_scope("Read.Write") diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index de05169d..1526da4e 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -6,7 +6,7 @@ fn sign_in_code_url() { // Test the sign in url with a manually set response type. let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .response_type("code") @@ -25,7 +25,7 @@ fn sign_in_code_url_with_state() { // Test the sign in url with a manually set response type. let mut oauth = OAuth::new(); oauth - .authorize_url("https://example.com/oauth2/v2.0/authorize") + .authorization_url("https://example.com/oauth2/v2.0/authorize") .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .response_type("code") @@ -46,8 +46,8 @@ fn access_token() { .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .client_secret("CLDIE3F") - .authorize_url("https://www.example.com/token") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + .authorization_url("https://www.example.com/token") + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); let mut builder = AccessToken::default(); builder @@ -72,8 +72,8 @@ fn refresh_token() { .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .client_secret("CLDIE3F") - .authorize_url("https://www.example.com/token") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + .authorization_url("https://www.example.com/token") + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); let mut access_token = AccessToken::new("access_token", 3600, "Read.Write", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); @@ -95,9 +95,9 @@ fn get_refresh_token() { .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .client_secret("CLDIE3F") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .refresh_token_url("https://www.example.com/token?") - .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") + .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); let mut access_token = AccessToken::new("access_token", 3600, "Read.Write", "asfasf"); @@ -118,7 +118,7 @@ fn multi_scope() { .add_scope("Files.ReadWrite.All") .add_scope("wl.offline_access") .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .access_token_url("https://login.live.com/oauth20_token.srf") .refresh_token_url("https://login.live.com/oauth20_token.srf") .response_type("code") diff --git a/tests/grants_implicit.rs b/tests/grants_implicit.rs index c4f91fce..9f5fa65a 100644 --- a/tests/grants_implicit.rs +++ b/tests/grants_implicit.rs @@ -4,7 +4,7 @@ use graph_rs_sdk::oauth::{GrantRequest, GrantType, OAuth}; pub fn implicit_grant_url() { let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") .add_scope("Read") .add_scope("Read.Write") diff --git a/tests/grants_openid.rs b/tests/grants_openid.rs index e5cffa63..a384ab85 100644 --- a/tests/grants_openid.rs +++ b/tests/grants_openid.rs @@ -5,7 +5,7 @@ use url::{Host, Url}; pub fn oauth() -> OAuth { let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.microsoftonline.com/common/oauth2/authorize") + .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .response_type("id_token") .redirect_uri("http://localhost:8080") diff --git a/tests/grants_token_flow.rs b/tests/grants_token_flow.rs index 94112ee7..0e864f7d 100644 --- a/tests/grants_token_flow.rs +++ b/tests/grants_token_flow.rs @@ -5,7 +5,7 @@ use graph_rs_sdk::oauth::{GrantRequest, OAuth}; pub fn token_flow_url() { let mut oauth = OAuth::new(); oauth - .authorize_url("https://login.live.com/oauth20_authorize.srf?") + .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") .add_scope("Read") .add_scope("Read.Write") diff --git a/tests/oauth_tests.rs b/tests/oauth_tests.rs index 5fb61a2a..42da1b90 100644 --- a/tests/oauth_tests.rs +++ b/tests/oauth_tests.rs @@ -9,11 +9,11 @@ fn oauth_parameters_from_credential() { oauth .client_id("client_id") .client_secret("client_secret") - .authorize_url("https://example.com/authorize?") + .authorization_url("https://example.com/authorize?") .access_token_url("https://example.com/token?") .refresh_token_url("https://example.com/token?") .redirect_uri("https://example.com/redirect?") - .access_code("ADSLFJL4L3") + .authorization_code("ADSLFJL4L3") .response_mode("response_mode") .response_type("response_type") .state("state") @@ -39,23 +39,23 @@ fn oauth_parameters_from_credential() { OAuthCredential::ClientSecret => { assert_eq!(oauth.get(credential), Some("client_secret".into())) } - OAuthCredential::AuthorizeURL => assert_eq!( + OAuthCredential::AuthorizationUrl => assert_eq!( oauth.get(credential), Some("https://example.com/authorize?".into()) ), - OAuthCredential::AccessTokenURL => assert_eq!( + OAuthCredential::AccessTokenUrl => assert_eq!( oauth.get(credential), Some("https://example.com/token?".into()) ), - OAuthCredential::RefreshTokenURL => assert_eq!( + OAuthCredential::RefreshTokenUrl => assert_eq!( oauth.get(credential), Some("https://example.com/token?".into()) ), - OAuthCredential::RedirectURI => assert_eq!( + OAuthCredential::RedirectUri => assert_eq!( oauth.get(credential), Some("https://example.com/redirect?".into()) ), - OAuthCredential::AccessCode => { + OAuthCredential::AuthorizationCode => { assert_eq!(oauth.get(credential), Some("ADSLFJL4L3".into())) } OAuthCredential::ResponseMode => { @@ -108,18 +108,18 @@ fn remove_credential() { .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .client_secret("CLDIE3F") - .authorize_url("https://www.example.com/authorize?") + .authorization_url("https://www.example.com/authorize?") .refresh_token_url("https://www.example.com/token?") - .access_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); assert!(oauth.get(OAuthCredential::ClientId).is_some()); oauth.remove(OAuthCredential::ClientId); assert!(oauth.get(OAuthCredential::ClientId).is_none()); oauth.client_id("client_id"); assert!(oauth.get(OAuthCredential::ClientId).is_some()); - assert!(oauth.get(OAuthCredential::RedirectURI).is_some()); - oauth.remove(OAuthCredential::RedirectURI); - assert!(oauth.get(OAuthCredential::RedirectURI).is_none()); + assert!(oauth.get(OAuthCredential::RedirectUri).is_some()); + oauth.remove(OAuthCredential::RedirectUri); + assert!(oauth.get(OAuthCredential::RedirectUri).is_none()); } #[test] @@ -130,11 +130,11 @@ fn setters() { oauth .client_id("client_id") .client_secret("client_secret") - .authorize_url("https://example.com/authorize") + .authorization_url("https://example.com/authorize") .refresh_token_url("https://example.com/token") .access_token_url("https://example.com/token") .redirect_uri("https://example.com/redirect") - .access_code("access_code"); + .authorization_code("access_code"); let test_setter = |c: OAuthCredential, s: &str| { let result = oauth.get(c); @@ -146,14 +146,14 @@ fn setters() { test_setter(OAuthCredential::ClientId, "client_id"); test_setter(OAuthCredential::ClientSecret, "client_secret"); test_setter( - OAuthCredential::AuthorizeURL, + OAuthCredential::AuthorizationUrl, "https://example.com/authorize", ); test_setter( - OAuthCredential::RefreshTokenURL, + OAuthCredential::RefreshTokenUrl, "https://example.com/token", ); - test_setter(OAuthCredential::AccessTokenURL, "https://example.com/token"); - test_setter(OAuthCredential::RedirectURI, "https://example.com/redirect"); - test_setter(OAuthCredential::AccessCode, "access_code"); + test_setter(OAuthCredential::AccessTokenUrl, "https://example.com/token"); + test_setter(OAuthCredential::RedirectUri, "https://example.com/redirect"); + test_setter(OAuthCredential::AuthorizationCode, "access_code"); } From be366447a5b7d44f974cea8d76f03e375b6d67ed Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 16 Apr 2023 06:16:05 -0400 Subject: [PATCH 003/118] Add client credentials secret and certificate credential --- Cargo.toml | 1 + examples/oauth/client_credentials.rs | 112 ++------------ .../oauth/client_credentials_admin_consent.rs | 116 ++++++++++++++ examples/oauth/main.rs | 3 +- graph-oauth/src/auth.rs | 20 ++- graph-oauth/src/identity/authority.rs | 18 +++ ...thorization_code_certificate_credential.rs | 23 ++- .../authorization_code_credential.rs | 107 +++++-------- .../client_certificate_credential.rs | 72 +++++++++ .../client_credentials_authorization_url.rs | 133 +++++++++++++++++ .../credentials/client_secret_credential.rs | 141 ++++++++++++++++++ ....rs => confidential_client_application.rs} | 43 ++++-- graph-oauth/src/identity/credentials/mod.rs | 10 +- graph-oauth/src/lib.rs | 1 + tests/grants_authorization_code.rs | 2 +- tests/grants_code_flow.rs | 2 +- 16 files changed, 608 insertions(+), 196 deletions(-) create mode 100644 examples/oauth/client_credentials_admin_consent.rs create mode 100644 graph-oauth/src/identity/credentials/client_certificate_credential.rs create mode 100644 graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs create mode 100644 graph-oauth/src/identity/credentials/client_secret_credential.rs rename graph-oauth/src/identity/credentials/{confidential_client.rs => confidential_client_application.rs} (72%) diff --git a/Cargo.toml b/Cargo.toml index 5063a149..31c1fb89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ futures = "0.3" lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } warp = "0.3.3" +webbrowser = "0.8.7" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index 5cfe39ce..c5dcc107 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -1,16 +1,3 @@ -use graph_oauth::oauth::AccessToken; -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -/// -/// # Overview: -/// /// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) /// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, /// sometimes called two-legged OAuth, to access web-hosted resources by using the @@ -22,95 +9,28 @@ use graph_oauth::oauth::AccessToken; /// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent /// only has to be done once for a user. After admin consent is given, the oauth client can be /// used to continue getting new access tokens programmatically. -use graph_rs_sdk::oauth::OAuth; -use warp::Filter; +use graph_rs_sdk::oauth::{ + AccessToken, ClientSecretCredential, ConfidentialClientApplication, TokenRequest, +}; + +// This example shows programmatically getting an access token using the client credentials +// flow after admin consent has been granted. If you have not granted admin consent, see +// examples/client_credentials_admin_consent.rs for more info. // The client_id and client_secret must be changed before running this example. static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct ClientCredentialsResponse { - admin_consent: bool, - tenant: String, -} - -fn get_oauth_client() -> OAuth { - let mut oauth = OAuth::new(); - oauth - .client_id(CLIENT_ID) - .client_secret(CLIENT_SECRET) - .add_scope("https://graph.microsoft.com/.default") - .redirect_uri("http://localhost:8000/redirect") - .authorization_url("https://login.microsoftonline.com/common/adminconsent") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token"); - oauth -} - -async fn request_access_token() { - let mut oauth = get_oauth_client(); - let mut request = oauth.build_async().client_credentials(); +pub async fn get_token_silent() { + let client_secret_credential = ClientSecretCredential::new(CLIENT_ID, CLIENT_SECRET); + let mut confidential_client_application = + ConfidentialClientApplication::from(client_secret_credential); - let response = request.access_token().send().await.unwrap(); + let response = confidential_client_application + .get_token_silent_async() + .await + .unwrap(); println!("{response:#?}"); - if response.status().is_success() { - let access_token: AccessToken = response.json().await.unwrap(); - - println!("{access_token:#?}"); - oauth.access_token(access_token); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - } -} - -async fn handle_redirect( - client_credential_option: Option<ClientCredentialsResponse>, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - match client_credential_option { - Some(client_credential_response) => { - // Print out for debugging purposes. - println!("{client_credential_response:#?}"); - - // Request an access token. - request_access_token().await; - - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } -} - -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<ClientCredentialsResponse>() - .map(Some) - .or_else(|_| async { - Ok::<(Option<ClientCredentialsResponse>,), std::convert::Infallible>((None,)) - }); - - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); - - // Get the oauth client and request a browser sign in - let mut oauth = get_oauth_client(); - let mut request = oauth.build_async().client_credentials(); - request.browser_authorization().open().unwrap(); - - warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; + let body: AccessToken = response.json().await.unwrap(); } diff --git a/examples/oauth/client_credentials_admin_consent.rs b/examples/oauth/client_credentials_admin_consent.rs new file mode 100644 index 00000000..abcb7988 --- /dev/null +++ b/examples/oauth/client_credentials_admin_consent.rs @@ -0,0 +1,116 @@ +use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrl; +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +/// +/// # Overview: +/// +/// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +/// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, +/// sometimes called two-legged OAuth, to access web-hosted resources by using the +/// identity of an application. This type of grant is commonly used for server-to-server +/// interactions that must run in the background, without immediate interaction with a user. +/// These types of applications are often referred to as daemons or service accounts. +/// +/// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, +/// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent +/// only has to be done once for a user. After admin consent is given, the oauth client can be +/// used to continue getting new access tokens programmatically. +use warp::Filter; + +// This example shows getting the URL for the one time admin consent required +// for the client credentials flow. +// See https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#request-the-permissions-from-a-directory-admin + +// Once an admin has given consent the ClientSecretCredential can be +// used to get access tokens programmatically without any consent by a user +// or admin. + +// Paste the URL into a browser and have the admin sign in and approve the admin consent. +fn example_authorization_credential() { + let authorization_credential = + ClientCredentialsAuthorizationUrl::new("<CLIENT_ID>", "<REDIRECT_URI>"); + let url = authorization_credential.url(); +} + +// Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. +fn example_builder() { + let mut builder = ClientCredentialsAuthorizationUrl::builder(); + let authorization_credential = builder + .with_client_id("<CLIENT_ID>") + .with_redirect_uri("<REDIRECT_URI>") + .with_state("<STATE>") + .with_tenant("<TENANT_ID>") + .build(); + let url = authorization_credential.url().unwrap(); +} + +// ------------------------------------------------------------------------------------------------- +// Full example with handling redirect: + +// After admin consent has been granted see examples/client_credential.rs for how to +// programmatically get access tokens using the client credentials flow. + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct ClientCredentialsResponse { + admin_consent: bool, + tenant: String, +} + +async fn handle_redirect( + client_credential_option: Option<ClientCredentialsResponse>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match client_credential_option { + Some(client_credential_response) => { + // Print out for debugging purposes. + println!("{client_credential_response:#?}"); + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} + +// The client_id must be changed before running this example. +static CLIENT_ID: &str = "<CLIENT_ID>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<ClientCredentialsResponse>() + .map(Some) + .or_else(|_| async { + Ok::<(Option<ClientCredentialsResponse>,), std::convert::Infallible>((None,)) + }); + + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); + + // Get the oauth client and request a browser sign in + let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI); + let url = authorization_credential.url().unwrap(); + + // webbrowser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); + + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index ae9d214e..e2e79bdf 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -18,6 +18,7 @@ extern crate serde; mod auth_code_grant; mod auth_code_grant_pkce; mod client_credentials; +mod client_credentials_admin_consent; mod code_flow; mod implicit_grant; mod is_access_token_expired; @@ -35,7 +36,7 @@ async fn main() { auth_code_grant_pkce::start_server_main().await; // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow - client_credentials::start_server_main().await; + client_credentials_admin_consent::start_server_main().await; // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code code_flow::start_server_main().await; diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 1eff20dd..15902014 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -389,6 +389,19 @@ impl OAuth { .refresh_token_url(&token_url) } + pub fn authority_admin_consent( + &mut self, + host: &AzureAuthorityHost, + authority: &Authority, + ) -> &mut OAuth { + let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); + let auth_url = format!("{}/{}/adminconsent", host.as_ref(), authority.as_ref()); + + self.authorization_url(&auth_url) + .access_token_url(&token_url) + .refresh_token_url(&token_url) + } + /// Set the redirect url of a request /// /// # Example @@ -1086,8 +1099,6 @@ impl OAuth { } GrantRequest::RefreshToken => { let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); - let refresh_token = self.get_refresh_token()?; - encoder.append_pair("refresh_token", &refresh_token); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1110,7 +1121,6 @@ impl OAuth { let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); } else { let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); - encoder.append_pair("refresh_token", &self.get_refresh_token()?); } self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(request_type), &mut encoder); Ok(encoder.finish()) @@ -1157,8 +1167,6 @@ impl OAuth { } GrantRequest::RefreshToken => { let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); - let refresh_token = self.get_refresh_token()?; - encoder.append_pair("refresh_token", &refresh_token); self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1370,7 +1378,7 @@ impl GrantSelector<AccessTokenGrant> { } } - /// Create a new instance for the open id connect grant. + /// Create a new instance for the client credentials grant. /// /// # See /// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 5bd44dc3..8e6c2f5f 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -38,6 +38,24 @@ impl TryFrom<AzureAuthorityHost> for Url { } } +impl AzureAuthorityHost { + pub fn default_microsoft_graph_scope(&self) -> &'static str { + "https://graph.microsoft.com/.default" + } + + pub fn default_managed_identity_scope(&self) -> &'static str { + match self { + AzureAuthorityHost::Custom(_) => "https://management.azure.com//.default", + AzureAuthorityHost::AzurePublic => "https://management.azure.com//.default", + AzureAuthorityHost::AzureChina => "https://management.chinacloudapi.cn/.default", + AzureAuthorityHost::AzureGermany => "https://management.microsoftazure.de/.default", + AzureAuthorityHost::AzureUsGovernment => { + "https://management.usgovcloudapi.net/.default" + } + } + } +} + #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Authority { #[default] diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 20d77e51..63de80a0 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,7 +1,9 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use crate::identity::{ + Authority, AuthorizationCodeAuthorizationUrl, AuthorizationSerializer, AzureAuthorityHost, +}; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use std::collections::HashMap; @@ -255,7 +257,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { self } - pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { self.authorization_code_credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); self @@ -265,3 +267,20 @@ impl AuthorizationCodeCertificateCredentialBuilder { self.authorization_code_credential.clone() } } + +impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBuilder { + fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { + let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); + builder + .with_scope(value.scopes) + .with_client_id(value.client_id) + .with_redirect_uri(value.redirect_uri) + .with_authority(value.authority); + + if let Some(code_verifier) = value.code_verifier.as_ref() { + builder.with_code_verifier(code_verifier); + } + + builder + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 506a5e89..31d91ebe 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -27,25 +27,6 @@ pub struct AuthorizationCodeCredential { serializer: OAuth, } -/* - pub(crate) client_id: String, - pub(crate) redirect_uri: String, - pub(crate) authority: Authority, - pub(crate) response_mode: ResponseMode, - pub(crate) response_type: String, - pub(crate) nonce: Option<String>, - pub(crate) state: Option<String>, - pub(crate) scopes: Vec<String>, - pub(crate) prompt: Option<Prompt>, - pub(crate) domain_hint: Option<String>, - pub(crate) login_hint: Option<String>, - pub(crate) code_challenge: Option<String>, - pub(crate) code_challenge_method: Option<String>, - - authority - -*/ - impl AuthorizationCodeCredential { pub fn new<T: AsRef<str>>( client_id: T, @@ -80,10 +61,17 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self - .serializer - .get_or_else(OAuthCredential::AccessTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + if self.refresh_token.is_some() { + let uri = self + .serializer + .get_or_else(OAuthCredential::AccessTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) + } else { + let uri = self + .serializer + .get_or_else(OAuthCredential::RefreshTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) + } } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -174,13 +162,13 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { #[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { - authorization_code_credential: AuthorizationCodeCredential, + credential: AuthorizationCodeCredential, } impl AuthorizationCodeCredentialBuilder { fn new() -> AuthorizationCodeCredentialBuilder { Self { - authorization_code_credential: AuthorizationCodeCredential { + credential: AuthorizationCodeCredential { authorization_code: None, refresh_token: None, client_id: String::new(), @@ -194,82 +182,55 @@ impl AuthorizationCodeCredentialBuilder { } } - pub fn with_authorization_code<T: AsRef<str>>( - &mut self, - authorization_code: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.authorization_code = - Some(authorization_code.as_ref().to_owned()); + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self } - pub fn with_refresh_token<T: AsRef<str>>( - &mut self, - refresh_token: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.authorization_code = None; - self.authorization_code_credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.authorization_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } - pub fn with_redirect_uri<T: AsRef<str>>( - &mut self, - redirect_uri: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.redirect_uri = redirect_uri.as_ref().to_owned(); + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); self } - pub fn with_client_id<T: AsRef<str>>( - &mut self, - client_id: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.client_id = client_id.as_ref().to_owned(); + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); self } - pub fn with_client_secret<T: AsRef<str>>( - &mut self, - client_secret: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.client_secret = client_secret.as_ref().to_owned(); + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.credential.client_secret = client_secret.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.authorization_code_credential.authority = - Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } - pub fn with_authority<T: Into<Authority>>( - &mut self, - authority: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.authority = authority.into(); + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); self } - pub fn with_code_verifier<T: AsRef<str>>( - &mut self, - code_verifier: T, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); self } - pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>( - &mut self, - scopes: I, - ) -> &mut AuthorizationCodeCredentialBuilder { - self.authorization_code_credential.scopes = - scopes.into_iter().map(|s| s.to_string()).collect(); + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); self } pub fn build(&self) -> AuthorizationCodeCredential { - self.authorization_code_credential.clone() + self.credential.clone() } } @@ -277,7 +238,7 @@ impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCredentialBuil fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); builder - .with_scopes(value.scopes) + .with_scope(value.scopes) .with_client_id(value.client_id) .with_redirect_uri(value.redirect_uri) .with_authority(value.authority); @@ -320,7 +281,7 @@ mod test { .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_client_secret("client_secret") - .with_scopes(vec!["scope"]) + .with_scope(vec!["scope"]) .with_tenant("tenant_id"); let mut credential = credential_builder.build(); let _ = credential.form().unwrap(); diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs new file mode 100644 index 00000000..cba74b79 --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -0,0 +1,72 @@ +use crate::auth::OAuth; +use crate::identity::Authority; + +#[derive(Clone)] +#[allow(dead_code)] +pub struct ClientCertificateCredential { + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) certificate: String, + /// The value passed for the scope parameter in this request should be the resource + /// identifier (application ID URI) of the resource you want, affixed with the .default + /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// Default is https://graph.microsoft.com/.default. + pub(crate) scopes: Vec<String>, + pub(crate) authority: Authority, + serializer: OAuth, +} + +impl ClientCertificateCredential { + pub fn builder() -> ClientCertificateCredentialBuilder { + ClientCertificateCredentialBuilder::new() + } +} + +pub struct ClientCertificateCredentialBuilder { + credential: ClientCertificateCredential, +} + +impl ClientCertificateCredentialBuilder { + fn new() -> ClientCertificateCredentialBuilder { + ClientCertificateCredentialBuilder { + credential: ClientCertificateCredential { + client_id: String::new(), + certificate: String::new(), + scopes: vec![], + authority: Default::default(), + serializer: OAuth::new(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_certificate<T: AsRef<str>>(&mut self, certificate: T) -> &mut Self { + self.credential.certificate = certificate.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + /// Defaults to "https://graph.microsoft.com/.default" + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn build(&self) -> ClientCertificateCredential { + self.credential.clone() + } +} diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs new file mode 100644 index 00000000..a46d5078 --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -0,0 +1,133 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::{Authority, AzureAuthorityHost}; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use url::form_urlencoded::Serializer; +use url::Url; + +#[derive(Clone)] +pub struct ClientCredentialsAuthorizationUrl { + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) redirect_uri: String, + pub(crate) state: Option<String>, + pub(crate) authority: Authority, +} + +impl ClientCredentialsAuthorizationUrl { + pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> ClientCredentialsAuthorizationUrl { + ClientCredentialsAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + state: None, + authority: Default::default(), + } + } + + pub fn builder() -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.url_with_host(&AzureAuthorityHost::AzurePublic) + } + + pub fn url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { + let mut serializer = OAuth::new(); + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + } + + if self.redirect_uri.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::RedirectUri.alias(), + None, + ); + } + + serializer + .client_id(self.client_id.as_str()) + .redirect_uri(self.redirect_uri.as_str()); + + if let Some(state) = self.state.as_ref() { + serializer.state(state.as_ref()); + } + + serializer.authority_admin_consent(azure_authority_host, &self.authority); + + let mut encoder = Serializer::new(String::new()); + serializer.form_encode_credentials( + vec![ + OAuthCredential::ClientId, + OAuthCredential::RedirectUri, + OAuthCredential::State, + ], + &mut encoder, + ); + + let mut url = Url::parse( + serializer + .get_or_else(OAuthCredential::AuthorizationUrl) + .or(AuthorizationFailure::required_value( + OAuthCredential::AuthorizationUrl.alias(), + None, + ))? + .as_str(), + ) + .or(AuthorizationFailure::required_value( + OAuthCredential::AuthorizationUrl.alias(), + None, + ))?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } +} + +pub struct ClientCredentialsAuthorizationUrlBuilder { + credential: ClientCredentialsAuthorizationUrl, +} + +impl ClientCredentialsAuthorizationUrlBuilder { + fn new() -> Self { + Self { + credential: ClientCredentialsAuthorizationUrl { + client_id: String::new(), + redirect_uri: String::new(), + state: None, + authority: Default::default(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { + self.credential.state = Some(state.as_ref().to_owned()); + self + } + + pub fn build(&self) -> ClientCredentialsAuthorizationUrl { + self.credential.clone() + } +} diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs new file mode 100644 index 00000000..775e0d4c --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -0,0 +1,141 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::form_credential::FormCredential; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use std::collections::HashMap; +use url::Url; + +/// Client Credentials flow using a client secret. +/// +/// The OAuth 2.0 client credentials grant flow permits a web service (confidential client) +/// to use its own credentials, instead of impersonating a user, to authenticate when calling +/// another web service. The grant specified in RFC 6749, sometimes called two-legged OAuth, +/// can be used to access web-hosted resources by using the identity of an application. +/// This type is commonly used for server-to-server interactions that must run in the background, +/// without immediate interaction with a user, and is often referred to as daemons or service accounts. +/// +/// See [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +#[derive(Clone)] +pub struct ClientSecretCredential { + /// The client (application) ID of the service principal + pub(crate) client_id: String, + pub(crate) client_secret: String, + /// The value passed for the scope parameter in this request should be the resource + /// identifier (application ID URI) of the resource you want, affixed with the .default + /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// Default is https://graph.microsoft.com/.default. + pub(crate) scopes: Vec<String>, + pub(crate) authority: Authority, + serializer: OAuth, +} + +impl ClientSecretCredential { + pub fn new<T: AsRef<str>>(client_id: T, client_secret: T) -> ClientSecretCredential { + ClientSecretCredential { + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + scopes: vec!["https://graph.microsoft.com/.default".to_owned()], + authority: Default::default(), + serializer: OAuth::new(), + } + } + + pub fn builder() -> ClientSecretCredentialBuilder { + ClientSecretCredentialBuilder::new() + } +} + +impl AuthorizationSerializer for ClientSecretCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + let uri = self + .serializer + .get_or_else(OAuthCredential::AccessTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + } + + if self.client_secret.trim().is_empty() { + return AuthorizationFailure::required_value( + OAuthCredential::ClientSecret.alias(), + None, + ); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .grant_type("client_credentials"); + + if self.scopes.is_empty() { + self.serializer + .extend_scopes(vec!["https://graph.microsoft.com/.default".to_owned()]); + } else { + self.serializer.extend_scopes(&self.scopes); + } + + self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ClientSecret), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scopes), + ]) + } +} + +pub struct ClientSecretCredentialBuilder { + credential: ClientSecretCredential, +} + +impl ClientSecretCredentialBuilder { + fn new() -> Self { + Self { + credential: ClientSecretCredential { + client_id: String::new(), + client_secret: String::new(), + scopes: vec![], + authority: Default::default(), + serializer: OAuth::new(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.credential.client_secret = client_secret.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + /// Defaults to "https://graph.microsoft.com/.default" + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + self + } +} + +impl Default for ClientSecretCredentialBuilder { + fn default() -> Self { + Self::new() + } +} diff --git a/graph-oauth/src/identity/credentials/confidential_client.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs similarity index 72% rename from graph-oauth/src/identity/credentials/confidential_client.rs rename to graph-oauth/src/identity/credentials/confidential_client_application.rs index fac975e6..62b0a6ff 100644 --- a/graph-oauth/src/identity/credentials/confidential_client.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,21 +1,24 @@ use crate::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - TokenCredentialOptions, TokenRequest, + ClientSecretCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::GraphResult; use reqwest::Response; -pub struct ConfidentialClient { +pub struct ConfidentialClientApplication { http_client: reqwest::Client, credential: Box<dyn AuthorizationSerializer + Send>, token_credential_options: TokenCredentialOptions, } -impl ConfidentialClient { - pub fn new<T>(credential: T, options: TokenCredentialOptions) -> GraphResult<ConfidentialClient> +impl ConfidentialClientApplication { + pub fn new<T>( + credential: T, + options: TokenCredentialOptions, + ) -> GraphResult<ConfidentialClientApplication> where - T: Into<ConfidentialClient>, + T: Into<ConfidentialClientApplication>, { let mut confidential_client = credential.into(); confidential_client.token_credential_options = options; @@ -24,7 +27,7 @@ impl ConfidentialClient { } #[async_trait] -impl TokenRequest for ConfidentialClient { +impl TokenRequest for ConfidentialClientApplication { fn get_token_silent(&mut self) -> anyhow::Result<reqwest::blocking::Response> { let uri = self .credential @@ -43,9 +46,9 @@ impl TokenRequest for ConfidentialClient { } } -impl From<AuthorizationCodeCredential> for ConfidentialClient { +impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCredential) -> Self { - ConfidentialClient { + ConfidentialClientApplication { http_client: reqwest::Client::new(), credential: Box::new(value), token_credential_options: Default::default(), @@ -53,9 +56,19 @@ impl From<AuthorizationCodeCredential> for ConfidentialClient { } } -impl From<AuthorizationCodeCertificateCredential> for ConfidentialClient { +impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCertificateCredential) -> Self { - ConfidentialClient { + ConfidentialClientApplication { + http_client: reqwest::Client::new(), + credential: Box::new(value), + token_credential_options: Default::default(), + } + } +} + +impl From<ClientSecretCredential> for ConfidentialClientApplication { + fn from(value: ClientSecretCredential) -> Self { + ConfidentialClientApplication { http_client: reqwest::Client::new(), credential: Box::new(value), token_credential_options: Default::default(), @@ -74,12 +87,13 @@ mod test { .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") - .with_scopes(vec!["Read.Write", "Fall.Down"]) + .with_scope(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") .build(); let mut confidential_client = - ConfidentialClient::new(credential, TokenCredentialOptions::default()).unwrap(); + ConfidentialClientApplication::new(credential, TokenCredentialOptions::default()) + .unwrap(); let credential_uri = confidential_client .credential .uri(&AzureAuthorityHost::AzurePublic) @@ -97,12 +111,13 @@ mod test { .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") - .with_scopes(vec!["Read.Write", "Fall.Down"]) + .with_scope(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") .with_authority(Authority::Consumers) .build(); let mut confidential_client = - ConfidentialClient::new(credential, TokenCredentialOptions::default()).unwrap(); + ConfidentialClientApplication::new(credential, TokenCredentialOptions::default()) + .unwrap(); let credential_uri = confidential_client .credential .uri(&AzureAuthorityHost::AzurePublic) diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 8250e5af..2a605d5b 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,7 +1,10 @@ mod authorization_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; -mod confidential_client; +mod client_certificate_credential; +mod client_credentials_authorization_url; +mod client_secret_credential; +mod confidential_client_application; mod prompt; mod response_mode; mod token_credential; @@ -10,7 +13,10 @@ mod token_request; pub use authorization_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; -pub use confidential_client::*; +pub use client_certificate_credential::*; +pub use client_credentials_authorization_url::*; +pub use client_secret_credential::*; +pub use confidential_client_application::*; pub use prompt::*; pub use response_mode::*; pub use token_credential::*; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 7ad890e4..020cc76c 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -108,6 +108,7 @@ pub mod oauth { pub use crate::grants::GrantRequest; pub use crate::grants::GrantType; pub use crate::id_token::IdToken; + pub use crate::identity::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; } diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index d25c1ed0..75afa0a5 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -76,7 +76,7 @@ fn refresh_token_uri() { .encode_uri(GrantType::AuthorizationCode, GrantRequest::RefreshToken) .unwrap(); let test_url = - "refresh_token=32LKLASDKJ&client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&grant_type=refresh_token&scope=Fall.Down+Read.Write"; + "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&refresh_token=32LKLASDKJ&grant_type=refresh_token&scope=Fall.Down+Read.Write"; assert_eq!(test_url, body); } diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index 1526da4e..9e2e11dc 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -84,7 +84,7 @@ fn refresh_token() { .unwrap(); assert_eq!( body, - "refresh_token=32LKLASDKJ&client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&grant_type=refresh_token&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL".to_string() + "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&grant_type=refresh_token&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL&refresh_token=32LKLASDKJ".to_string() ); } From 561eb1c20c90fd7c042205be73115179b0b05c3c Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 19 Apr 2023 00:48:48 -0400 Subject: [PATCH 004/118] Update examples for client credentials and auth code grant --- Cargo.toml | 1 + examples/oauth/auth_code_grant.rs | 157 +++++++++--------- examples/oauth/auth_code_grant_pkce.rs | 121 ++++++-------- examples/oauth/client_credentials.rs | 22 +-- .../oauth/client_credentials_admin_consent.rs | 81 +++++---- examples/oauth/main.rs | 38 +++++ examples/test2.rs | 127 ++++++++++++++ graph-error/Cargo.toml | 1 + graph-error/src/authorization_failure.rs | 21 ++- graph-oauth/src/auth.rs | 75 ++++++++- graph-oauth/src/grants.rs | 22 +-- .../authorization_code_authorization_url.rs | 146 +++++++--------- ...thorization_code_certificate_credential.rs | 28 ++-- .../authorization_code_credential.rs | 40 +++-- .../client_credentials_authorization_url.rs | 11 +- .../credentials/client_secret_credential.rs | 9 +- graph-oauth/src/identity/credentials/mod.rs | 2 + .../proof_key_for_code_exchange.rs | 70 ++++++++ test-tools/src/oauth.rs | 4 +- 19 files changed, 634 insertions(+), 342 deletions(-) create mode 100644 examples/test2.rs create mode 100644 graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs diff --git a/Cargo.toml b/Cargo.toml index 31c1fb89..69f0786a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } warp = "0.3.3" webbrowser = "0.8.7" +anyhow = "1.0.69" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 4d593ee3..4498cacc 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,74 +1,70 @@ -use graph_rs_sdk::oauth::{AccessToken, OAuth}; -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` +use graph_rs_sdk::oauth::{ + AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + ConfidentialClientApplication, TokenRequest, +}; use graph_rs_sdk::*; use warp::Filter; +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; + #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AccessCode { code: String, } -fn oauth_client() -> OAuth { - let mut oauth = OAuth::new(); - oauth - .client_id("<YOUR_CLIENT_ID>") - .client_secret("<YOUR_CLIENT_SECRET>") - .add_scope("files.read") - .add_scope("files.readwrite") - .add_scope("files.read.all") - .add_scope("files.readwrite.all") - .add_scope("offline_access") - .redirect_uri("http://localhost:8000/redirect") - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .response_type("code"); - oauth +pub fn authorization_sign_in() { + let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + .with_client_id(CLIENT_ID) + .with_redirect_uri("http://localhost:8000/redirect") + .with_scope(vec!["offline_access", "files.read"]) + .build(); + + let url = auth_url_builder.url().unwrap(); + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); } -pub async fn set_and_req_access_code(access_code: AccessCode) -> GraphResult<()> { - let mut oauth = oauth_client(); - // The response type is automatically set to token and the grant type is automatically - // set to authorization_code if either of these were not previously set. - // This is done here as an example. - oauth.authorization_code(access_code.code.as_str()); - let mut request = oauth.build_async().authorization_code_grant(); +pub fn get_confidential_client(authorization_code: &str) -> ConfidentialClientApplication { + let auth_code_credential = AuthorizationCodeCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_scope(vec!["files.read", "offline_access"]) + .with_redirect_uri("http://localhost:8000/redirect") + .build(); - // Returns reqwest::Response - let response = request.access_token().send().await?; - println!("{response:#?}"); + ConfidentialClientApplication::from(auth_code_credential) +} - if response.status().is_success() { - let mut access_token: AccessToken = response.json().await?; +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - // Option<&JsonWebToken> - let jwt = access_token.jwt(); - println!("{jwt:#?}"); + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); - // Store in OAuth to make requests for refresh tokens. - oauth.access_token(access_token); + let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + .with_client_id(CLIENT_ID) + .with_redirect_uri("http://localhost:8000/redirect") + .with_scope(vec!["offline_access", "files.read"]) + .build(); - // If all went well here we can print out the OAuth config with the Access Token. - println!("{:#?}", &oauth); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; + authorization_sign_in(); - match result { - Ok(body) => println!("{body:#?}"), - Err(err) => println!("Error on deserialization:\n{err:#?}"), - } - } - - Ok(()) + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } async fn handle_redirect( @@ -82,7 +78,7 @@ async fn handle_redirect( // Set the access code and request an access token. // Callers should handle the Result from requesting an access token // in case of an error here. - set_and_req_access_code(access_code).await; + set_and_req_access_code(access_code).await.unwrap(); // Generic login page response. Ok(Box::new( @@ -93,28 +89,35 @@ async fn handle_redirect( } } -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<AccessCode>() - .map(Some) - .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); +async fn set_and_req_access_code(access_code: AccessCode) -> anyhow::Result<()> { + let mut confidential_client_application = get_confidential_client(access_code.code.as_str()); - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); + let response = confidential_client_application + .get_token_silent_async() + .await?; + println!("{response:#?}"); - let mut oauth = oauth_client(); - let mut request = oauth.build_async().authorization_code_grant(); - request.browser_authorization().open().unwrap(); + if response.status().is_success() { + let mut access_token: AccessToken = response.json().await?; - warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; + // Option<&JsonWebToken> + let jwt = access_token.jwt(); + println!("{jwt:#?}"); + + println!("{:#?}", access_token); + + // This will print the actual access token to the console. + println!("Access Token: {:#?}", access_token.bearer_token()); + println!("Refresh Token: {:#?}", access_token.refresh_token()); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + + match result { + Ok(body) => println!("{body:#?}"), + Err(err) => println!("Error on deserialization:\n{err:#?}"), + } + } + + Ok(()) } diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 8a5d5280..2c4934bb 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,27 +1,23 @@ -use graph_oauth::oauth::AccessToken; -use graph_rs_sdk::oauth::OAuth; +use graph_rs_sdk::error::AuthorizationResult; +use graph_rs_sdk::oauth::{ + AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, +}; use lazy_static::lazy_static; -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -use warp::Filter; - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct AccessCode { - code: String, -} +use warp::{get, Filter}; static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +// You can also pass your own values for PKCE instead of automatic generation by +// calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) lazy_static! { - static ref OAUTH_CLIENT: OAuthClient = OAuthClient::new(CLIENT_ID, CLIENT_SECRET); + static ref PKCE: ProofKeyForCodeExchange = ProofKeyForCodeExchange::generate().unwrap(); +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, } // This example shows how to use a code_challenge and code_verifier @@ -31,42 +27,39 @@ lazy_static! { // For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow // And the PKCE RFC: https://tools.ietf.org/html/rfc7636 -// Store and initialize OAuth within another struct so that we can -// use it in lazy_static since OAuth requires being mutable to -// change its fields. -// Although probably not suitable for production use for this example -// we will just clone the internal oauth each time we need it. -// We use lazy static to ensure the code verifier and code challenge -// stays the same between requests. -struct OAuthClient { - client: OAuth, +// Open the default system web browser to the sign in url for authorization. +// This method uses AuthorizationCodeAuthorizationUrl to build the sign in +// url and query needed to get an authorization code and opens the default system +// web browser to this Url. +fn authorization_sign_in() { + let auth_code_url_builder = AuthorizationCodeAuthorizationUrl::builder() + .with_client_id(CLIENT_ID) + .with_scope(vec!["user.read"]) + .with_redirect_uri("http://localhost:8000/redirect") + .with_proof_key_for_code_exchange(&PKCE) + .build(); + + let url = auth_code_url_builder.url().unwrap(); + webbrowser::open(url.as_str()).unwrap(); } -impl OAuthClient { - pub fn new(client_id: &str, client_secret: &str) -> OAuthClient { - let mut oauth = OAuth::new(); - oauth - .client_id(client_id) - .client_secret(client_secret) - .add_scope("user.read") - .add_scope("user.readwrite") - .redirect_uri("http://localhost:8000/redirect") - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .response_type("code"); - - // Generate the code challenge and code verifier. - oauth.generate_sha256_challenge_and_verifier().unwrap(); - - OAuthClient { client: oauth } - } - - pub fn oauth(&self) -> OAuth { - self.client.clone() - } +/// Build the Authorization Code Grant Credential. +fn get_confidential_client_application(authorization_code: &str) -> ConfidentialClientApplication { + let credential = AuthorizationCodeCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_redirect_uri("http://localhost:8000/redirect") + .with_proof_key_for_code_exchange(&PKCE) + .build(); + + ConfidentialClientApplication::from(credential) } +// When the authorization code comes in on the redirect from sign in, call the get_credential +// method passing in the authorization code. The AuthorizationCodeCredential can be passed +// to a confidential client application in order to exchange the authorization code +// for an access token. async fn handle_redirect( code_option: Option<AccessCode>, ) -> Result<Box<dyn warp::Reply>, warp::Rejection> { @@ -75,31 +68,25 @@ async fn handle_redirect( // Print out the code for debugging purposes. println!("{:#?}", access_code.code); - // Set the access code and request an access token. - // Callers should handle the Result from requesting an access token - // in case of an error here. - let mut oauth = OAUTH_CLIENT.oauth(); - - oauth.authorization_code(access_code.code.as_str()); - let mut request = oauth.build_async().authorization_code_grant(); + let mut confidential_client = + get_confidential_client_application(access_code.code.as_str()); // Returns reqwest::Response - let response = request.access_token().send().await.unwrap(); + let response = confidential_client.get_token_silent_async().await.unwrap(); println!("{response:#?}"); - if !response.status().is_success() { + if response.status().is_success() { + let access_token: AccessToken = response.json().await.unwrap(); + + // If all went well here we can print out the OAuth config with the Access Token. + println!("AccessToken: {:#?}", access_token.bearer_token()); + } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; println!("{result:#?}"); return Ok(Box::new("Error Logging In! You can close your browser.")); } - let access_token: AccessToken = response.json().await.unwrap(); - oauth.access_token(access_token); - - // If all went well here we can print out the OAuth config with the Access Token. - println!("{:#?}", &oauth); - // Generic login page response. Ok(Box::new( "Successfully Logged In! You can close your browser.", @@ -128,9 +115,7 @@ pub async fn start_server_main() { .and(query) .and_then(handle_redirect); - let mut oauth = OAUTH_CLIENT.oauth(); - let mut request = oauth.build_async().authorization_code_grant(); - request.browser_authorization().open().unwrap(); + authorization_sign_in(); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index c5dcc107..71261927 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -1,14 +1,14 @@ -/// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) -/// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, -/// sometimes called two-legged OAuth, to access web-hosted resources by using the -/// identity of an application. This type of grant is commonly used for server-to-server -/// interactions that must run in the background, without immediate interaction with a user. -/// These types of applications are often referred to as daemons or service accounts. -/// -/// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, -/// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent -/// only has to be done once for a user. After admin consent is given, the oauth client can be -/// used to continue getting new access tokens programmatically. +// Microsoft Client Credentials: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, +// sometimes called two-legged OAuth, to access web-hosted resources by using the +// identity of an application. This type of grant is commonly used for server-to-server +// interactions that must run in the background, without immediate interaction with a user. +// These types of applications are often referred to as daemons or service accounts. +// +// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, +// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent +// only has to be done once for a user. After admin consent is given, the oauth client can be +// used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ AccessToken, ClientSecretCredential, ConfidentialClientApplication, TokenRequest, }; diff --git a/examples/oauth/client_credentials_admin_consent.rs b/examples/oauth/client_credentials_admin_consent.rs index abcb7988..d38c1e3e 100644 --- a/examples/oauth/client_credentials_admin_consent.rs +++ b/examples/oauth/client_credentials_admin_consent.rs @@ -1,28 +1,14 @@ -use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrl; -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -/// -/// # Overview: -/// -/// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) -/// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, -/// sometimes called two-legged OAuth, to access web-hosted resources by using the -/// identity of an application. This type of grant is commonly used for server-to-server -/// interactions that must run in the background, without immediate interaction with a user. -/// These types of applications are often referred to as daemons or service accounts. -/// -/// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, -/// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent -/// only has to be done once for a user. After admin consent is given, the oauth client can be -/// used to continue getting new access tokens programmatically. -use warp::Filter; +// Microsoft Client Credentials: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow +// You can use the OAuth 2.0 client credentials grant specified in RFC 6749, +// sometimes called two-legged OAuth, to access web-hosted resources by using the +// identity of an application. This type of grant is commonly used for server-to-server +// interactions that must run in the background, without immediate interaction with a user. +// These types of applications are often referred to as daemons or service accounts. +// +// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, +// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent +// only has to be done once for a user. After admin consent is given, the oauth client can be +// used to continue getting new access tokens programmatically. // This example shows getting the URL for the one time admin consent required // for the client credentials flow. @@ -30,30 +16,42 @@ use warp::Filter; // Once an admin has given consent the ClientSecretCredential can be // used to get access tokens programmatically without any consent by a user -// or admin. +// or admin. See examples/client_credentials.rs + +use graph_rs_sdk::error::AuthorizationResult; +use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrl; +use warp::Filter; + +// The client_id must be changed before running this example. +static CLIENT_ID: &str = "<CLIENT_ID>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // Paste the URL into a browser and have the admin sign in and approve the admin consent. -fn example_authorization_credential() { - let authorization_credential = - ClientCredentialsAuthorizationUrl::new("<CLIENT_ID>", "<REDIRECT_URI>"); - let url = authorization_credential.url(); +fn get_admin_consent_url() -> AuthorizationResult<url::Url> { + let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI); + authorization_credential.url() } +// OR use the builder: + // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. -fn example_builder() { - let mut builder = ClientCredentialsAuthorizationUrl::builder(); - let authorization_credential = builder - .with_client_id("<CLIENT_ID>") - .with_redirect_uri("<REDIRECT_URI>") - .with_state("<STATE>") - .with_tenant("<TENANT_ID>") +fn get_admin_consent_url_from_builder() -> AuthorizationResult<url::Url> { + let authorization_credential = ClientCredentialsAuthorizationUrl::builder() + .with_client_id(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI) + .with_state("123") + .with_tenant("tenant_id") .build(); - let url = authorization_credential.url().unwrap(); + authorization_credential.url() } // ------------------------------------------------------------------------------------------------- // Full example with handling redirect: +// Start a server and listen for the redirect url passed to the client +// credentials url. This should be the same redirect Uri that is in +// Azure Active Directory. + // After admin consent has been granted see examples/client_credential.rs for how to // programmatically get access tokens using the client credentials flow. @@ -80,10 +78,6 @@ async fn handle_redirect( } } -// The client_id must be changed before running this example. -static CLIENT_ID: &str = "<CLIENT_ID>"; -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; - /// # Example /// ``` /// use graph_rs_sdk::*: @@ -106,8 +100,7 @@ pub async fn start_server_main() { .and_then(handle_redirect); // Get the oauth client and request a browser sign in - let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI); - let url = authorization_credential.url().unwrap(); + let url = get_admin_consent_url().unwrap(); // webbrowser crate in dev dependencies will open to default browser in the system. webbrowser::open(url.as_str()).unwrap(); diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index e2e79bdf..4b1b5ecb 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -26,6 +26,11 @@ mod logout; mod open_id_connect; mod signing_keys; +use graph_rs_sdk::oauth::{ + AccessToken, AuthorizationCodeCredential, ClientSecretCredential, + ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, +}; + #[tokio::main] async fn main() { // Some examples of what you can use for authentication and getting access tokens. There are @@ -44,3 +49,36 @@ async fn main() { // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc open_id_connect::start_server_main().await; } + +// Examples + +// Authorization Code Grant +fn auth_code_grant(authorization_code: &str) -> AuthorizationCodeCredential { + let pkce = ProofKeyForCodeExchange::generate().unwrap(); + + AuthorizationCodeCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id("CLIENT_ID") + .with_client_secret("CLIENT_SECRET") + .with_redirect_uri("http://localhost:8000/redirect") + .with_proof_key_for_code_exchange(&pkce) + .build() +} + +// Client Credentials Grant +fn client_credentials() { + pub async fn get_token_silent() { + let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); + let mut confidential_client_application = + ConfidentialClientApplication::from(client_secret_credential); + + let response = confidential_client_application + .get_token_silent_async() + .await + .unwrap(); + println!("{response:#?}"); + + let access_token: AccessToken = response.json().await.unwrap(); + println!("{:#?}", access_token.bearer_token()); + } +} diff --git a/examples/test2.rs b/examples/test2.rs new file mode 100644 index 00000000..fd590061 --- /dev/null +++ b/examples/test2.rs @@ -0,0 +1,127 @@ +use graph_error::AuthorizationResult; +use graph_rs_sdk::oauth::{ + AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, +}; +use lazy_static::lazy_static; +use warp::{get, Filter}; + +#[macro_use] +extern crate serde; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, +} + +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; + +// You can also pass your own values for PKCE instead of automatic generation by +// calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) +lazy_static! { + static ref PKCE: ProofKeyForCodeExchange = ProofKeyForCodeExchange::generate().unwrap(); +} + +// This example shows how to use a code_challenge and code_verifier +// to perform the authorization code grant flow with proof key for +// code exchange (PKCE). +// +// For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +// And the PKCE RFC: https://tools.ietf.org/html/rfc7636 + +// Open the default system web browser to the sign in url for authorization. +// This method uses AuthorizationCodeAuthorizationUrl to build the sign in +// url and query needed to get an authorization code and opens the default system +// web browser to this Url. +pub fn authorization_sign_in() { + let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + .with_client_id("e0951f73-cafa-455f-9365-50dfd22f56b6") + .with_redirect_uri("http://localhost:8000/redirect") + .with_scope(vec!["offline_access", "files.read"]) + .build(); + + let url = auth_url_builder.url().unwrap(); + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); +} + +pub fn get_confidential_client(authorization_code: &str) -> ConfidentialClientApplication { + let auth_code_credential = AuthorizationCodeCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id("e0951f73-cafa-455f-9365-50dfd22f56b6") + .with_client_secret("rUWHfYygz~IZH~7I~2.w1-Sedf~T16g8OR") + .with_scope(vec!["files.read", "offline_access"]) + .with_redirect_uri("http://localhost:8000/redirect") + .build(); + + ConfidentialClientApplication::from(auth_code_credential) +} + +// When the authorization code comes in on the redirect from sign in, call the get_credential +// method passing in the authorization code. The AuthorizationCodeCredential can be passed +// to a confidential client application in order to exchange the authorization code +// for an access token. +async fn handle_redirect( + code_option: Option<AccessCode>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match code_option { + Some(access_code) => { + // Print out the code for debugging purposes. + println!("{:#?}", access_code.code); + + let mut confidential_client = get_confidential_client(access_code.code.as_str()); + + // Returns reqwest::Response + let response = confidential_client.get_token_silent_async().await.unwrap(); + println!("{response:#?}"); + + if response.status().is_success() { + let access_token: AccessToken = response.json().await.unwrap(); + + // If all went well here we can print out the OAuth config with the Access Token. + println!("AccessToken: {:#?}", access_token.bearer_token()); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + println!("{result:#?}"); + return Ok(Box::new("Error Logging In! You can close your browser.")); + } + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); + + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); + + authorization_sign_in(); + + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +} + +#[tokio::main] +async fn main() { + start_server_main().await; +} diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index d1894ceb..19d9e666 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -23,3 +23,4 @@ serde_json = "1" thiserror = "1" tokio = { version = "1.25.0", features = ["full"] } url = "2" +x509-parser = "0.15.0" diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index b120eae9..3e7c66da 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,3 +1,5 @@ +use crate::AuthorizationResult; + #[derive(Debug, thiserror::Error)] pub enum AuthorizationFailure { #[error("Required value missing:\n{0:#?}", name)] @@ -5,13 +7,30 @@ pub enum AuthorizationFailure { name: String, message: Option<String>, }, + + #[error("{0:#?}")] + UrlParseError(#[from] url::ParseError), } impl AuthorizationFailure { - pub fn required_value<T>(name: &str, message: Option<&str>) -> Result<T, AuthorizationFailure> { + pub fn required_value<T: AsRef<str>, U>(name: T) -> AuthorizationResult<U> { + Err(AuthorizationFailure::RequiredValue { + name: name.as_ref().to_owned(), + message: None, + }) + } + + pub fn required_value_msg<T>( + name: &str, + message: Option<&str>, + ) -> Result<T, AuthorizationFailure> { Err(AuthorizationFailure::RequiredValue { name: name.to_owned(), message: message.map(|s| s.to_owned()), }) } + + pub fn url_parse_error<T>(url_parse_error: url::ParseError) -> Result<T, AuthorizationFailure> { + Err(AuthorizationFailure::UrlParseError(url_parse_error)) + } } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 15902014..4a0664de 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -40,7 +40,7 @@ pub enum OAuthCredential { IdToken, Resource, DomainHint, - Scopes, + Scope, LoginHint, ClientAssertion, ClientAssertionType, @@ -76,7 +76,7 @@ impl OAuthCredential { OAuthCredential::IdToken => "id_token", OAuthCredential::Resource => "resource", OAuthCredential::DomainHint => "domain_hint", - OAuthCredential::Scopes => "scope", + OAuthCredential::Scope => "scope", OAuthCredential::LoginHint => "login_hint", OAuthCredential::ClientAssertion => "client_assertion", OAuthCredential::ClientAssertionType => "client_assertion_type", @@ -113,6 +113,12 @@ impl ToString for OAuthCredential { } } +impl AsRef<str> for OAuthCredential { + fn as_ref(&self) -> &'static str { + self.alias() + } +} + /// # OAuth /// /// OAuth client implementing the OAuth 2.0 and OpenID Connect protocols @@ -246,7 +252,7 @@ impl OAuth { /// println!("{:#?}", oauth.contains(OAuthCredential::Nonce)); /// ``` pub fn contains(&self, t: OAuthCredential) -> bool { - if t == OAuthCredential::Scopes { + if t == OAuthCredential::Scope { return !self.scopes.is_empty(); } self.credentials.contains_key(t.alias()) @@ -1004,6 +1010,57 @@ impl OAuth { }); } + fn query_encode_filter(&self, form_credential: &FormCredential) -> bool { + let oac = { + match form_credential { + FormCredential::Required(oac) => *oac, + FormCredential::NotRequired(oac) => *oac, + } + }; + self.contains_key(oac.alias()) || oac.alias().eq("scope") + } + + pub fn url_query_encode( + &mut self, + pairs: Vec<FormCredential>, + encoder: &mut Serializer<String>, + ) -> AuthorizationResult<()> { + for form_credential in pairs.iter() { + if self.query_encode_filter(form_credential) { + match form_credential { + FormCredential::Required(oac) => { + if oac.alias().eq("scope") { + if self.scopes.is_empty() { + return AuthorizationFailure::required_value_msg::<()>( + oac.alias(), + None, + ); + } else { + encoder.append_pair("scope", self.join_scopes(" ").as_str()); + } + } else if let Some(val) = self.get(*oac) { + encoder.append_pair(oac.alias(), val.as_str()); + } else { + return AuthorizationFailure::required_value_msg::<()>( + oac.alias(), + None, + ); + } + } + FormCredential::NotRequired(oac) => { + if oac.alias().eq("scope") && !self.scopes.is_empty() { + encoder.append_pair("scope", self.join_scopes(" ").as_str()); + } else if let Some(val) = self.get(*oac) { + encoder.append_pair(oac.alias(), val.as_str()); + } + } + } + } + } + + Ok(()) + } + pub fn params(&mut self, pairs: Vec<OAuthCredential>) -> GraphResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for oac in pairs.iter() { @@ -1035,13 +1092,19 @@ impl OAuth { name: oac.alias().into(), message: None, })?; - map.insert(oac.to_string(), val); + if val.trim().is_empty() { + return AuthorizationFailure::required_value(oac); + } else { + map.insert(oac.to_string(), val); + } } FormCredential::NotRequired(oac) => { - if oac.eq(&OAuthCredential::Scopes) && !self.scopes.is_empty() { + if oac.eq(&OAuthCredential::Scope) && !self.scopes.is_empty() { map.insert("scope".into(), self.join_scopes(" ")); } else if let Some(val) = self.get(*oac) { - map.insert(oac.to_string(), val); + if !val.trim().is_empty() { + map.insert(oac.to_string(), val); + } } } } diff --git a/graph-oauth/src/grants.rs b/graph-oauth/src/grants.rs index d25cdbd6..e70e0f53 100644 --- a/graph-oauth/src/grants.rs +++ b/graph-oauth/src/grants.rs @@ -32,7 +32,7 @@ impl GrantType { OAuthCredential::ClientId, OAuthCredential::RedirectUri, OAuthCredential::ResponseType, - OAuthCredential::Scopes, + OAuthCredential::Scope, ], }, GrantType::CodeFlow => match grant_request { @@ -41,7 +41,7 @@ impl GrantType { OAuthCredential::RedirectUri, OAuthCredential::State, OAuthCredential::ResponseType, - OAuthCredential::Scopes, + OAuthCredential::Scope, ], GrantRequest::AccessToken => vec![ OAuthCredential::ClientId, @@ -67,7 +67,7 @@ impl GrantType { OAuthCredential::State, OAuthCredential::ResponseMode, OAuthCredential::ResponseType, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::Prompt, OAuthCredential::DomainHint, OAuthCredential::LoginHint, @@ -79,7 +79,7 @@ impl GrantType { OAuthCredential::ClientSecret, OAuthCredential::RedirectUri, OAuthCredential::AuthorizationCode, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::GrantType, OAuthCredential::CodeVerifier, ], @@ -88,7 +88,7 @@ impl GrantType { OAuthCredential::ClientSecret, OAuthCredential::RefreshToken, OAuthCredential::GrantType, - OAuthCredential::Scopes, + OAuthCredential::Scope, ], }, GrantType::Implicit => match grant_request { @@ -97,7 +97,7 @@ impl GrantType { | GrantRequest::RefreshToken => vec![ OAuthCredential::ClientId, OAuthCredential::RedirectUri, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::ResponseType, OAuthCredential::ResponseMode, OAuthCredential::State, @@ -113,7 +113,7 @@ impl GrantType { OAuthCredential::ResponseType, OAuthCredential::RedirectUri, OAuthCredential::ResponseMode, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::State, OAuthCredential::Nonce, OAuthCredential::Prompt, @@ -126,7 +126,7 @@ impl GrantType { OAuthCredential::ClientSecret, OAuthCredential::RedirectUri, OAuthCredential::GrantType, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::AuthorizationCode, OAuthCredential::CodeVerifier, ], @@ -135,7 +135,7 @@ impl GrantType { OAuthCredential::ClientSecret, OAuthCredential::RefreshToken, OAuthCredential::GrantType, - OAuthCredential::Scopes, + OAuthCredential::Scope, ], }, GrantType::ClientCredentials => match grant_request { @@ -148,7 +148,7 @@ impl GrantType { OAuthCredential::ClientId, OAuthCredential::ClientSecret, OAuthCredential::GrantType, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::ClientAssertion, OAuthCredential::ClientAssertionType, ], @@ -162,7 +162,7 @@ impl GrantType { OAuthCredential::GrantType, OAuthCredential::Username, OAuthCredential::Password, - OAuthCredential::Scopes, + OAuthCredential::Scope, OAuthCredential::RedirectUri, OAuthCredential::ClientAssertion, ], diff --git a/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs b/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs index 836e63a5..7ccd4fc5 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs @@ -1,12 +1,9 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; use crate::identity::{Authority, AzureAuthorityHost, Prompt, ResponseMode}; -use crate::oauth::OAuthError; -use base64::Engine; -use ring::rand::SecureRandom; - -use graph_error::{GraphFailure, GraphResult}; - +use crate::oauth::form_credential::FormCredential; +use crate::oauth::ProofKeyForCodeExchange; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -29,18 +26,14 @@ pub struct AuthorizationCodeAuthorizationUrl { pub(crate) client_id: String, pub(crate) redirect_uri: String, pub(crate) authority: Authority, - pub(crate) response_mode: ResponseMode, pub(crate) response_type: String, + pub(crate) response_mode: Option<ResponseMode>, pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, pub(crate) prompt: Option<Prompt>, pub(crate) domain_hint: Option<String>, pub(crate) login_hint: Option<String>, - /// The code verifier is not included in the authorization URL. - /// You can set the code verifier here and then use the From trait - /// for [AuthorizationCodeCredential] which does use the code verifier. - pub(crate) code_verifier: Option<String>, pub(crate) code_challenge: Option<String>, pub(crate) code_challenge_method: Option<String>, } @@ -51,15 +44,14 @@ impl AuthorizationCodeAuthorizationUrl { client_id: client_id.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), authority: Authority::default(), - response_mode: ResponseMode::Query, response_type: "code".to_owned(), + response_mode: None, nonce: None, state: None, - scopes: vec![], + scope: vec![], prompt: None, domain_hint: None, login_hint: None, - code_verifier: None, code_challenge: None, code_challenge_method: None, } @@ -73,29 +65,46 @@ impl AuthorizationCodeAuthorizationUrl { AuthorizationCodeAuthorizationUrlBuilder::new() } - pub fn url(&self) -> GraphResult<Url> { + pub fn url(&self) -> AuthorizationResult<Url> { self.url_with_host(&AzureAuthorityHost::default()) } - pub fn url_with_host(&self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + pub fn url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { let mut serializer = OAuth::new(); if self.redirect_uri.trim().is_empty() { - return OAuthError::error_from(OAuthCredential::RedirectUri); + return AuthorizationFailure::required_value_msg("redirect_uri", None); } if self.client_id.trim().is_empty() { - return OAuthError::error_from(OAuthCredential::ClientId); + return AuthorizationFailure::required_value_msg("client_id", None); + } + + if self.response_type.trim().is_empty() { + return AuthorizationFailure::required_value_msg( + "response_type", + Some("Must include code for the authorization code flow. Can also include id_token or token if using the hybrid flow.") + ); + } + + if self.scope.is_empty() { + return AuthorizationFailure::required_value_msg("scope", None); } serializer .client_id(self.client_id.as_str()) .redirect_uri(self.redirect_uri.as_str()) - .extend_scopes(self.scopes.clone()) + .extend_scopes(self.scope.clone()) .authority(azure_authority_host, &self.authority) - .response_mode(self.response_mode.as_ref()) .response_type(self.response_type.as_str()); + if let Some(response_mode) = self.response_mode.as_ref() { + serializer.response_mode(response_mode.as_ref()); + } + if let Some(state) = self.state.as_ref() { serializer.state(state.as_str()); } @@ -121,30 +130,29 @@ impl AuthorizationCodeAuthorizationUrl { } let authorization_credentials = vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::State, - OAuthCredential::ResponseMode, - OAuthCredential::ResponseType, - OAuthCredential::Scopes, - OAuthCredential::Prompt, - OAuthCredential::DomainHint, - OAuthCredential::LoginHint, - OAuthCredential::CodeChallenge, - OAuthCredential::CodeChallengeMethod, + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ResponseType), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::NotRequired(OAuthCredential::ResponseMode), + FormCredential::NotRequired(OAuthCredential::State), + FormCredential::NotRequired(OAuthCredential::Prompt), + FormCredential::NotRequired(OAuthCredential::LoginHint), + FormCredential::NotRequired(OAuthCredential::DomainHint), + FormCredential::NotRequired(OAuthCredential::CodeChallenge), + FormCredential::NotRequired(OAuthCredential::CodeChallengeMethod), ]; let mut encoder = Serializer::new(String::new()); - serializer.form_encode_credentials(authorization_credentials, &mut encoder); - - let mut url = Url::parse( - serializer - .get_or_else(OAuthCredential::AuthorizationUrl)? - .as_str(), - ) - .map_err(GraphFailure::from)?; - url.set_query(Some(encoder.finish().as_str())); - Ok(url) + serializer.url_query_encode(authorization_credentials, &mut encoder)?; + + if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } else { + AuthorizationFailure::required_value_msg("authorization_url", None) + } } } @@ -166,15 +174,14 @@ impl AuthorizationCodeAuthorizationUrlBuilder { client_id: String::new(), redirect_uri: String::new(), authority: Authority::default(), - response_mode: ResponseMode::Query, + response_mode: None, response_type: "code".to_owned(), nonce: None, state: None, - scopes: vec![], + scope: vec![], prompt: None, domain_hint: None, login_hint: None, - code_verifier: None, code_challenge: None, code_challenge_method: None, }, @@ -222,7 +229,7 @@ impl AuthorizationCodeAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.authorization_code_authorize_url.response_mode = response_mode; + self.authorization_code_authorize_url.response_mode = Some(response_mode); self } @@ -240,8 +247,8 @@ impl AuthorizationCodeAuthorizationUrlBuilder { self } - pub fn with_scopes<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.authorization_code_authorize_url.scopes = + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.authorization_code_authorize_url.scope = scopes.into_iter().map(|s| s.to_string()).collect(); self } @@ -271,12 +278,6 @@ impl AuthorizationCodeAuthorizationUrlBuilder { self } - pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { - self.authorization_code_authorize_url.code_verifier = - Some(code_verifier.as_ref().to_owned()); - self - } - /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). /// Required if code_challenge_method is included. pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { @@ -299,34 +300,13 @@ impl AuthorizationCodeAuthorizationUrlBuilder { self } - /// Generate a code challenge and code verifier for the - /// authorization code grant flow using proof key for - /// code exchange (PKCE) and SHA256. - /// - /// This method automatically sets the code_verifier, - /// code_challenge, and code_challenge_method fields. - /// - /// For authorization, the code_challenge_method parameter in the request body - /// is automatically set to 'S256'. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn generate_sha256_challenge_and_verifier(&mut self) -> Result<(), GraphFailure> { - let mut buf = [0; 32]; - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf)?; - let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf); - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(verifier.as_bytes()); - let code_challenge = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - - self.with_code_verifier(verifier); - self.with_code_challenge(code_challenge); - self.with_code_challenge_method("S256"); - Ok(()) + pub fn with_proof_key_for_code_exchange( + &mut self, + proof_key_for_code_exchange: &ProofKeyForCodeExchange, + ) -> &mut Self { + self.with_code_challenge(proof_key_for_code_exchange.code_challenge.as_str()); + self.with_code_challenge_method(proof_key_for_code_exchange.code_challenge_method.as_str()); + self } pub fn build(&self) -> AuthorizationCodeAuthorizationUrl { @@ -343,7 +323,7 @@ mod test { let authorizer = AuthorizationCodeAuthorizationUrl::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") - .with_scopes(["read", "write"]) + .with_scope(["read", "write"]) .build(); let url_result = authorizer.url(); diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 63de80a0..086e6c19 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -6,7 +6,6 @@ use crate::identity::{ }; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use std::collections::HashMap; - use url::Url; #[derive(Clone)] @@ -75,7 +74,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -86,18 +85,21 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + return AuthorizationFailure::required_value_msg( + OAuthCredential::ClientId.alias(), + None, + ); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::ClientSecret.alias(), None, ); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::ClientAssertion.alias(), None, ); @@ -122,7 +124,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::RefreshToken.alias(), None, ); @@ -136,13 +138,13 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { FormCredential::Required(OAuthCredential::RefreshToken), FormCredential::Required(OAuthCredential::ClientId), FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::Scope), FormCredential::Required(OAuthCredential::ClientAssertion), FormCredential::Required(OAuthCredential::ClientAssertionType), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::AuthorizationCode.alias(), None, ); @@ -157,14 +159,14 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { FormCredential::Required(OAuthCredential::ClientId), FormCredential::Required(OAuthCredential::RedirectUri), FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::Scope), FormCredential::NotRequired(OAuthCredential::CodeVerifier), FormCredential::Required(OAuthCredential::ClientAssertion), FormCredential::Required(OAuthCredential::ClientAssertionType), ]); } - AuthorizationFailure::required_value( + AuthorizationFailure::required_value_msg( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -272,15 +274,11 @@ impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCertificateCre fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); builder - .with_scope(value.scopes) + .with_scope(value.scope) .with_client_id(value.client_id) .with_redirect_uri(value.redirect_uri) .with_authority(value.authority); - if let Some(code_verifier) = value.code_verifier.as_ref() { - builder.with_code_verifier(code_verifier); - } - builder } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 31d91ebe..64a666a6 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -3,6 +3,7 @@ use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; use crate::identity::{ Authority, AuthorizationCodeAuthorizationUrl, AuthorizationSerializer, AzureAuthorityHost, + ProofKeyForCodeExchange, }; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use std::collections::HashMap; @@ -61,7 +62,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { self.serializer .authority(azure_authority_host, &self.authority); - if self.refresh_token.is_some() { + if self.refresh_token.is_none() { let uri = self .serializer .get_or_else(OAuthCredential::AccessTokenUrl)?; @@ -76,7 +77,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -87,11 +88,14 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + return AuthorizationFailure::required_value_msg( + OAuthCredential::ClientId.alias(), + None, + ); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::ClientSecret.alias(), None, ); @@ -104,7 +108,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); @@ -119,16 +123,20 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { FormCredential::Required(OAuthCredential::ClientSecret), FormCredential::Required(OAuthCredential::RefreshToken), FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::Scope), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); } + if self.redirect_uri.trim().is_empty() { + return AuthorizationFailure::required_value(OAuthCredential::RedirectUri); + } + self.serializer .authorization_code(authorization_code.as_ref()) .grant_type("authorization_code") @@ -144,12 +152,12 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { FormCredential::Required(OAuthCredential::RedirectUri), FormCredential::Required(OAuthCredential::AuthorizationCode), FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::Scope), FormCredential::NotRequired(OAuthCredential::CodeVerifier), ]); } - AuthorizationFailure::required_value( + AuthorizationFailure::required_value_msg( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -224,6 +232,14 @@ impl AuthorizationCodeCredentialBuilder { self } + pub fn with_proof_key_for_code_exchange( + &mut self, + proof_key_for_code_exchange: &ProofKeyForCodeExchange, + ) -> &mut Self { + self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); + self + } + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); self @@ -238,15 +254,11 @@ impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCredentialBuil fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); builder - .with_scope(value.scopes) + .with_scope(value.scope) .with_client_id(value.client_id) .with_redirect_uri(value.redirect_uri) .with_authority(value.authority); - if let Some(code_verifier) = value.code_verifier.as_ref() { - builder.with_code_verifier(code_verifier); - } - builder } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index a46d5078..0b0dfe66 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -37,11 +37,14 @@ impl ClientCredentialsAuthorizationUrl { ) -> AuthorizationResult<Url> { let mut serializer = OAuth::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + return AuthorizationFailure::required_value_msg( + OAuthCredential::ClientId.alias(), + None, + ); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value( + return AuthorizationFailure::required_value_msg( OAuthCredential::RedirectUri.alias(), None, ); @@ -70,13 +73,13 @@ impl ClientCredentialsAuthorizationUrl { let mut url = Url::parse( serializer .get_or_else(OAuthCredential::AuthorizationUrl) - .or(AuthorizationFailure::required_value( + .or(AuthorizationFailure::required_value_msg( OAuthCredential::AuthorizationUrl.alias(), None, ))? .as_str(), ) - .or(AuthorizationFailure::required_value( + .or(AuthorizationFailure::required_value_msg( OAuthCredential::AuthorizationUrl.alias(), None, ))?; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 775e0d4c..fa537f31 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -58,14 +58,11 @@ impl AuthorizationSerializer for ClientSecretCredential { fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientId.alias(), None); + return AuthorizationFailure::required_value(OAuthCredential::ClientId); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value( - OAuthCredential::ClientSecret.alias(), - None, - ); + return AuthorizationFailure::required_value(OAuthCredential::ClientSecret); } self.serializer @@ -84,7 +81,7 @@ impl AuthorizationSerializer for ClientSecretCredential { FormCredential::Required(OAuthCredential::ClientId), FormCredential::Required(OAuthCredential::ClientSecret), FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scopes), + FormCredential::NotRequired(OAuthCredential::Scope), ]) } } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 2a605d5b..f92c0c14 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -6,6 +6,7 @@ mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; mod prompt; +mod proof_key_for_code_exchange; mod response_mode; mod token_credential; mod token_request; @@ -18,6 +19,7 @@ pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; pub use confidential_client_application::*; pub use prompt::*; +pub use proof_key_for_code_exchange::*; pub use response_mode::*; pub use token_credential::*; pub use token_request::*; diff --git a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs new file mode 100644 index 00000000..641291d9 --- /dev/null +++ b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs @@ -0,0 +1,70 @@ +use base64::Engine; +use ring::rand::SecureRandom; + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ProofKeyForCodeExchange { + /// The code verifier is not included in the authorization URL. + /// You can set the code verifier here and then use the From trait + /// for [AuthorizationCodeCredential] which does use the code verifier. + pub code_verifier: String, + /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). + /// Required if code_challenge_method is included. For more information, see the PKCE RFC. + /// This parameter is now recommended for all application types, both public and confidential + /// clients, and required by the Microsoft identity platform for single page apps using the + /// authorization code flow. + pub code_challenge: String, + /// The method used to encode the code_verifier for the code_challenge parameter. + /// This SHOULD be S256, but the spec allows the use of plain if the client can't support SHA256. + /// + /// If excluded, code_challenge is assumed to be plaintext if code_challenge is included. + /// The Microsoft identity platform supports both plain and S256. + /// For more information, see the PKCE RFC. This parameter is required for single page + /// apps using the authorization code flow. + pub code_challenge_method: String, +} + +impl ProofKeyForCodeExchange { + pub fn new<T: AsRef<str>>( + code_verifier: T, + code_challenge: T, + code_challenge_method: T, + ) -> ProofKeyForCodeExchange { + ProofKeyForCodeExchange { + code_verifier: code_verifier.as_ref().to_owned(), + code_challenge: code_challenge.as_ref().to_owned(), + code_challenge_method: code_challenge_method.as_ref().to_owned(), + } + } + + /// Generate a code challenge and code verifier for the + /// authorization code grant flow using proof key for + /// code exchange (PKCE) and SHA256. + /// + /// [ProofKeyForCodeExchange] contains a code_verifier, + /// code_challenge, and code_challenge_method for use in the authorization code grant. + /// + /// For authorization, the code_challenge_method parameter in the request body + /// is automatically set to 'S256'. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + pub fn generate() -> anyhow::Result<ProofKeyForCodeExchange> { + let mut buf = [0; 32]; + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + let code_verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf); + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(code_verifier.as_bytes()); + let code_challenge = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + + Ok(ProofKeyForCodeExchange { + code_verifier, + code_challenge, + code_challenge_method: "S256".to_owned(), + }) + } +} diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs index c990ddd9..baf4e69f 100644 --- a/test-tools/src/oauth.rs +++ b/test-tools/src/oauth.rs @@ -47,7 +47,7 @@ impl OAuthTestTool { for oac in OAuthCredential::iter() { if oauth.contains(oac) && includes.contains(&oac) && !not_includes.contains(&oac) { - if oac.eq(&OAuthCredential::Scopes) { + if oac.eq(&OAuthCredential::Scope) { let s = oauth.join_scopes(" "); cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { @@ -55,7 +55,7 @@ impl OAuthTestTool { cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } } else if oauth.contains(oac) && not_includes.contains(&oac) { - if oac.eq(&OAuthCredential::Scopes) { + if oac.eq(&OAuthCredential::Scope) { let s = oauth.join_scopes(" "); cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { From 07c16ae68170562218e30c9a99d23a0e3420dcaf Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 20 Apr 2023 22:59:38 -0400 Subject: [PATCH 005/118] Rename authorization url builder --- examples/oauth/auth_code_grant.rs | 67 +++++++++---------- examples/oauth/auth_code_grant_pkce.rs | 4 +- examples/test2.rs | 7 +- ..._url.rs => auth_code_authorization_url.rs} | 16 ++--- ...thorization_code_certificate_credential.rs | 40 +++++------ .../authorization_code_credential.rs | 6 +- graph-oauth/src/identity/credentials/mod.rs | 4 +- 7 files changed, 67 insertions(+), 77 deletions(-) rename graph-oauth/src/identity/credentials/{authorization_code_authorization_url.rs => auth_code_authorization_url.rs} (96%) diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 4498cacc..82ea8130 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, ConfidentialClientApplication, TokenRequest, }; use graph_rs_sdk::*; @@ -14,7 +14,7 @@ pub struct AccessCode { } pub fn authorization_sign_in() { - let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + let auth_url_builder = AuthCodeAuthorizationUrl::builder() .with_client_id(CLIENT_ID) .with_redirect_uri("http://localhost:8000/redirect") .with_scope(vec!["offline_access", "files.read"]) @@ -56,7 +56,7 @@ pub async fn start_server_main() { .and(query) .and_then(handle_redirect); - let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + let auth_url_builder = AuthCodeAuthorizationUrl::builder() .with_client_id(CLIENT_ID) .with_redirect_uri("http://localhost:8000/redirect") .with_scope(vec!["offline_access", "files.read"]) @@ -78,46 +78,41 @@ async fn handle_redirect( // Set the access code and request an access token. // Callers should handle the Result from requesting an access token // in case of an error here. - set_and_req_access_code(access_code).await.unwrap(); + let mut confidential_client_application = + get_confidential_client(access_code.code.as_str()); - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } -} + let response = confidential_client_application + .get_token_silent_async() + .await?; + println!("{response:#?}"); -async fn set_and_req_access_code(access_code: AccessCode) -> anyhow::Result<()> { - let mut confidential_client_application = get_confidential_client(access_code.code.as_str()); + if response.status().is_success() { + let mut access_token: AccessToken = response.json().await?; - let response = confidential_client_application - .get_token_silent_async() - .await?; - println!("{response:#?}"); + // Option<&JsonWebToken> + let jwt = access_token.jwt(); + println!("{jwt:#?}"); - if response.status().is_success() { - let mut access_token: AccessToken = response.json().await?; + println!("{:#?}", access_token); - // Option<&JsonWebToken> - let jwt = access_token.jwt(); - println!("{jwt:#?}"); + // This will print the actual access token to the console. + println!("Access Token: {:#?}", access_token.bearer_token()); + println!("Refresh Token: {:#?}", access_token.refresh_token()); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{:#?}", access_token); + match result { + Ok(body) => println!("{body:#?}"), + Err(err) => println!("Error on deserialization:\n{err:#?}"), + } + } - // This will print the actual access token to the console. - println!("Access Token: {:#?}", access_token.bearer_token()); - println!("Refresh Token: {:#?}", access_token.refresh_token()); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - - match result { - Ok(body) => println!("{body:#?}"), - Err(err) => println!("Error on deserialization:\n{err:#?}"), + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) } + None => Err(warp::reject()), } - - Ok(()) } diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 2c4934bb..d6bc4c53 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, }; use lazy_static::lazy_static; @@ -32,7 +32,7 @@ pub struct AccessCode { // url and query needed to get an authorization code and opens the default system // web browser to this Url. fn authorization_sign_in() { - let auth_code_url_builder = AuthorizationCodeAuthorizationUrl::builder() + let auth_code_url_builder = AuthCodeAuthorizationUrl::builder() .with_client_id(CLIENT_ID) .with_scope(vec!["user.read"]) .with_redirect_uri("http://localhost:8000/redirect") diff --git a/examples/test2.rs b/examples/test2.rs index fd590061..2fbe03e7 100644 --- a/examples/test2.rs +++ b/examples/test2.rs @@ -1,10 +1,9 @@ -use graph_error::AuthorizationResult; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeAuthorizationUrl, AuthorizationCodeCredential, + AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, }; use lazy_static::lazy_static; -use warp::{get, Filter}; +use warp::Filter; #[macro_use] extern crate serde; @@ -35,7 +34,7 @@ lazy_static! { // url and query needed to get an authorization code and opens the default system // web browser to this Url. pub fn authorization_sign_in() { - let auth_url_builder = AuthorizationCodeAuthorizationUrl::builder() + let auth_url_builder = AuthCodeAuthorizationUrl::builder() .with_client_id("e0951f73-cafa-455f-9365-50dfd22f56b6") .with_redirect_uri("http://localhost:8000/redirect") .with_scope(vec!["offline_access", "files.read"]) diff --git a/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs similarity index 96% rename from graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs rename to graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 7ccd4fc5..be0cbab2 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -21,7 +21,7 @@ use url::Url; /// /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code #[derive(Clone)] -pub struct AuthorizationCodeAuthorizationUrl { +pub struct AuthCodeAuthorizationUrl { /// The client (application) ID of the service principal pub(crate) client_id: String, pub(crate) redirect_uri: String, @@ -38,9 +38,9 @@ pub struct AuthorizationCodeAuthorizationUrl { pub(crate) code_challenge_method: Option<String>, } -impl AuthorizationCodeAuthorizationUrl { - pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthorizationCodeAuthorizationUrl { - AuthorizationCodeAuthorizationUrl { +impl AuthCodeAuthorizationUrl { + pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthCodeAuthorizationUrl { + AuthCodeAuthorizationUrl { client_id: client_id.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), authority: Authority::default(), @@ -158,7 +158,7 @@ impl AuthorizationCodeAuthorizationUrl { #[derive(Clone)] pub struct AuthorizationCodeAuthorizationUrlBuilder { - authorization_code_authorize_url: AuthorizationCodeAuthorizationUrl, + authorization_code_authorize_url: AuthCodeAuthorizationUrl, } impl Default for AuthorizationCodeAuthorizationUrlBuilder { @@ -170,7 +170,7 @@ impl Default for AuthorizationCodeAuthorizationUrlBuilder { impl AuthorizationCodeAuthorizationUrlBuilder { pub fn new() -> AuthorizationCodeAuthorizationUrlBuilder { AuthorizationCodeAuthorizationUrlBuilder { - authorization_code_authorize_url: AuthorizationCodeAuthorizationUrl { + authorization_code_authorize_url: AuthCodeAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), authority: Authority::default(), @@ -309,7 +309,7 @@ impl AuthorizationCodeAuthorizationUrlBuilder { self } - pub fn build(&self) -> AuthorizationCodeAuthorizationUrl { + pub fn build(&self) -> AuthCodeAuthorizationUrl { self.authorization_code_authorize_url.clone() } } @@ -320,7 +320,7 @@ mod test { #[test] fn serialize_uri() { - let authorizer = AuthorizationCodeAuthorizationUrl::builder() + let authorizer = AuthCodeAuthorizationUrl::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 086e6c19..1a87eb15 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; use crate::identity::{ - Authority, AuthorizationCodeAuthorizationUrl, AuthorizationSerializer, AzureAuthorityHost, + AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, }; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use std::collections::HashMap; @@ -179,13 +179,13 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { #[derive(Clone)] pub struct AuthorizationCodeCertificateCredentialBuilder { - authorization_code_credential: AuthorizationCodeCertificateCredential, + credential: AuthorizationCodeCertificateCredential, } impl AuthorizationCodeCertificateCredentialBuilder { fn new() -> AuthorizationCodeCertificateCredentialBuilder { Self { - authorization_code_credential: AuthorizationCodeCertificateCredential { + credential: AuthorizationCodeCertificateCredential { authorization_code: None, refresh_token: None, client_id: String::new(), @@ -202,51 +202,49 @@ impl AuthorizationCodeCertificateCredentialBuilder { } pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { - self.authorization_code_credential.authorization_code = - Some(authorization_code.as_ref().to_owned()); + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self } pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.authorization_code_credential.authorization_code = None; - self.authorization_code_credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self.credential.authorization_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.authorization_code_credential.redirect_uri = redirect_uri.as_ref().to_owned(); + self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); self } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.authorization_code_credential.client_id = client_id.as_ref().to_owned(); + self.credential.client_id = client_id.as_ref().to_owned(); self } pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { - self.authorization_code_credential.client_secret = client_secret.as_ref().to_owned(); + self.credential.client_secret = client_secret.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.authorization_code_credential.authority = - Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.authorization_code_credential.authority = authority.into(); + self.credential.authority = authority.into(); self } pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { - self.authorization_code_credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); self } pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { - self.authorization_code_credential.client_assertion = client_assertion.as_ref().to_owned(); + self.credential.client_assertion = client_assertion.as_ref().to_owned(); self } @@ -254,24 +252,22 @@ impl AuthorizationCodeCertificateCredentialBuilder { &mut self, client_assertion_type: T, ) -> &mut Self { - self.authorization_code_credential.client_assertion_type = - client_assertion_type.as_ref().to_owned(); + self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.authorization_code_credential.scopes = - scopes.into_iter().map(|s| s.to_string()).collect(); + self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); self } pub fn build(&self) -> AuthorizationCodeCertificateCredential { - self.authorization_code_credential.clone() + self.credential.clone() } } -impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBuilder { - fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { +impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBuilder { + fn from(value: AuthCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); builder .with_scope(value.scope) diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 64a666a6..75c92d47 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; use crate::identity::{ - Authority, AuthorizationCodeAuthorizationUrl, AuthorizationSerializer, AzureAuthorityHost, + AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, ProofKeyForCodeExchange, }; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; @@ -250,8 +250,8 @@ impl AuthorizationCodeCredentialBuilder { } } -impl From<AuthorizationCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { - fn from(value: AuthorizationCodeAuthorizationUrl) -> Self { +impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { + fn from(value: AuthCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); builder .with_scope(value.scope) diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index f92c0c14..b06ddcf9 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,4 +1,4 @@ -mod authorization_code_authorization_url; +mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; mod client_certificate_credential; @@ -11,7 +11,7 @@ mod response_mode; mod token_credential; mod token_request; -pub use authorization_code_authorization_url::*; +pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use client_certificate_credential::*; From bf47644657ab7c0f72dcac5541e9d1aa32724e5b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 23 Apr 2023 01:46:47 -0400 Subject: [PATCH 006/118] Add client assertion generation using openssl --- examples/oauth/auth_code_grant.rs | 5 +- examples/oauth/auth_code_grant_certificate.rs | 144 +++++++++++++ examples/oauth/auth_code_grant_pkce.rs | 2 +- examples/oauth/client_credentials.rs | 2 +- examples/oauth/main.rs | 3 +- graph-oauth/Cargo.toml | 3 + .../auth_code_authorization_url.rs | 18 +- ...thorization_code_certificate_credential.rs | 30 ++- .../identity/credentials/client_assertion.rs | 189 ++++++++++++++---- .../client_certificate_credential.rs | 42 +++- .../confidential_client_application.rs | 16 +- graph-oauth/src/identity/credentials/mod.rs | 2 + .../src/identity/credentials/token_request.rs | 4 +- graph-oauth/src/identity/mod.rs | 3 + 14 files changed, 381 insertions(+), 82 deletions(-) create mode 100644 examples/oauth/auth_code_grant_certificate.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index d6c13578..a3a64a5c 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -82,8 +82,9 @@ async fn handle_redirect( get_confidential_client(access_code.code.as_str()); let response = confidential_client_application - .get_token_silent_async() - .await.unwrap(); + .get_token_async() + .await + .unwrap(); println!("{response:#?}"); if response.status().is_success() { diff --git a/examples/oauth/auth_code_grant_certificate.rs b/examples/oauth/auth_code_grant_certificate.rs new file mode 100644 index 00000000..2e3e8d2b --- /dev/null +++ b/examples/oauth/auth_code_grant_certificate.rs @@ -0,0 +1,144 @@ +use graph_rs_sdk::oauth::{ + AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCertificateCredential, ClientAssertion, + ConfidentialClientApplication, PKey, Private, TokenRequest, X509, +}; +use std::fs::File; +use std::io::Read; +use warp::Filter; + +// This flow uses an X509 certificate for authorization. The public key should +// be uploaded to Azure Active Directory. In order to use the certificate +// flow the ClientAssertion struct can be used to generate the needed +// client assertion given an X509 certificate public key and private key. + +// If you want the client to generate a client assertion for you it +// requires the openssl feature be enabled. There are two openssl +// exports provided in this library: X509 and Pkey (private key) that will +// be used to generate the client assertion. You only need to provide these +// to the library in order to generate the client assertion. + +// You can use any way you want to get the public and private key. This example below uses +// File to get the contents of the X509 and private key, but if these files are local +// then consider using Rust's include_bytes macro which takes a local path to a file and returns the +// contents of that file as bytes. This is the expected format by X509 and Pkey in openssl. + +static CLIENT_ID: &str = "<CLIENT_ID>"; + +// Only required for certain applications. Used here as an example. +static TENANT: &str = "<TENANT_ID>"; + +// The path to the public key file. +static CERTIFICATE_PATH: &str = "<CERTIFICATE_PATH>"; + +// The path to the private key file of the certificate. +static PRIVATE_KEY_PATH: &str = "<PRIVATE_KEY_PATH>"; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, +} + +pub fn authorization_sign_in(client_id: &str, tenant_id: &str) { + let auth_url_builder = AuthCodeAuthorizationUrl::builder() + .with_client_id(client_id) + .with_tenant(tenant_id) + .with_redirect_uri("http://localhost:8080") + .with_scope(vec!["User.Read"]) + .build(); + + let url = auth_url_builder.url().unwrap(); + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); +} + +pub fn get_confidential_client( + authorization_code: &str, + client_id: &str, + tenant_id: &str, +) -> anyhow::Result<ConfidentialClientApplication> { + let mut cert_file = File::open(PRIVATE_KEY_PATH).unwrap(); + let mut certificate: Vec<u8> = Vec::new(); + cert_file.read_to_end(&mut cert); + + let mut private_key_file = File::open(CERTIFICATE_PATH).unwrap(); + let mut private_key: Vec<u8> = Vec::new(); + private_key_file.read_to_end(&mut cert); + + let cert = X509::from_pem(certificate.as_slice()).unwrap(); + let pkey = PKey::private_key_from_pem(private_key.as_slice()).unwrap(); + + let signed_client_assertion = + ClientAssertion::new_with_tenant(client_id, tenant_id, cert, pkey); + + let credentials = AuthorizationCodeCertificateCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id(client_id) + .with_tenant(tenant_id) + .with_certificate(&signed_client_assertion)? + .with_scope(vec!["User.Read"]) + .with_redirect_uri("http://localhost:8080") + .build(); + + Ok(ConfidentialClientApplication::from(credentials)) +} + +// When the authorization code comes in on the redirect from sign in, call the get_credential +// method passing in the authorization code. The AuthorizationCodeCertificateCredential can be passed +// to a confidential client application in order to exchange the authorization code +// for an access token. +async fn handle_redirect( + code_option: Option<AccessCode>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match code_option { + Some(access_code) => { + // Print out the code for debugging purposes. + println!("{:#?}", access_code.code); + + let mut confidential_client = + get_confidential_client(access_code.code.as_str(), CLIENT_ID, TENANT).unwrap(); + + // Returns reqwest::Response + let response = confidential_client.get_token_async().await.unwrap(); + println!("{response:#?}"); + + if response.status().is_success() { + let access_token: AccessToken = response.json().await.unwrap(); + + // If all went well here we can print out the Access Token. + println!("AccessToken: {:#?}", access_token.bearer_token()); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + println!("{result:#?}"); + return Ok(Box::new("Error Logging In! You can close your browser.")); + } + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); + + let routes = warp::get().and(query).and_then(handle_redirect); + + authorization_sign_in(CLIENT_ID, TENANT); + + warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +} diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index d6bc4c53..bd8f7073 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -72,7 +72,7 @@ async fn handle_redirect( get_confidential_client_application(access_code.code.as_str()); // Returns reqwest::Response - let response = confidential_client.get_token_silent_async().await.unwrap(); + let response = confidential_client.get_token_async().await.unwrap(); println!("{response:#?}"); if response.status().is_success() { diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index 71261927..f0882816 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -27,7 +27,7 @@ pub async fn get_token_silent() { ConfidentialClientApplication::from(client_secret_credential); let response = confidential_client_application - .get_token_silent_async() + .get_token_async() .await .unwrap(); println!("{response:#?}"); diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 1f4deb99..cda0709b 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -16,6 +16,7 @@ extern crate serde; mod auth_code_grant; +mod auth_code_grant_certificate; mod auth_code_grant_pkce; mod client_credentials; mod client_credentials_admin_consent; @@ -74,7 +75,7 @@ fn client_credentials() { ConfidentialClientApplication::from(client_secret_credential); let response = confidential_client_application - .get_token_silent_async() + .get_token_async() .await .unwrap(); println!("{response:#?}"); diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index fd4bacd5..0547565d 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -17,6 +17,8 @@ async-trait = "0.1.35" base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" +hex = "0.4.3" +openssl = "0.10" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" serde = { version = "1", features = ["derive"] } @@ -25,6 +27,7 @@ serde_json = "1" strum = { version = "0.24.1", features = ["derive"] } url = "2" webbrowser = "0.8.7" +uuid = { version = "1.3.1", features = ["v4"] } graph-error = { path = "../graph-error" } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index be0cbab2..878fe0ed 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -61,8 +61,8 @@ impl AuthCodeAuthorizationUrl { GrantType::AuthorizationCode } - pub fn builder() -> AuthorizationCodeAuthorizationUrlBuilder { - AuthorizationCodeAuthorizationUrlBuilder::new() + pub fn builder() -> AuthCodeAuthorizationUrlBuilder { + AuthCodeAuthorizationUrlBuilder::new() } pub fn url(&self) -> AuthorizationResult<Url> { @@ -157,19 +157,19 @@ impl AuthCodeAuthorizationUrl { } #[derive(Clone)] -pub struct AuthorizationCodeAuthorizationUrlBuilder { +pub struct AuthCodeAuthorizationUrlBuilder { authorization_code_authorize_url: AuthCodeAuthorizationUrl, } -impl Default for AuthorizationCodeAuthorizationUrlBuilder { +impl Default for AuthCodeAuthorizationUrlBuilder { fn default() -> Self { Self::new() } } -impl AuthorizationCodeAuthorizationUrlBuilder { - pub fn new() -> AuthorizationCodeAuthorizationUrlBuilder { - AuthorizationCodeAuthorizationUrlBuilder { +impl AuthCodeAuthorizationUrlBuilder { + pub fn new() -> AuthCodeAuthorizationUrlBuilder { + AuthCodeAuthorizationUrlBuilder { authorization_code_authorize_url: AuthCodeAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), @@ -210,8 +210,8 @@ impl AuthorizationCodeAuthorizationUrlBuilder { self } - /// Must include code for the authorization code flow. Can also include id_token or token - /// if using the hybrid flow. Default is code. + /// Default is code. Must include code for the authorization code flow. + /// Can also include id_token or token if using the hybrid flow. pub fn with_response_type<T: AsRef<str>>(&mut self, response_type: T) -> &mut Self { self.authorization_code_authorize_url.response_type = response_type.as_ref().to_owned(); self diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 1a87eb15..9db575b9 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -4,7 +4,9 @@ use crate::identity::form_credential::FormCredential; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, }; +use crate::oauth::ClientAssertion; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; + use std::collections::HashMap; use url::Url; @@ -17,7 +19,6 @@ pub struct AuthorizationCodeCertificateCredential { pub(crate) refresh_token: Option<String>, /// The client (application) ID of the service principal pub(crate) client_id: String, - pub(crate) client_secret: String, pub(crate) redirect_uri: String, pub(crate) code_verifier: Option<String>, pub(crate) client_assertion_type: String, @@ -31,7 +32,7 @@ pub struct AuthorizationCodeCertificateCredential { impl AuthorizationCodeCertificateCredential { pub fn new<T: AsRef<str>>( client_id: T, - client_secret: T, + _client_secret: T, authorization_code: T, redirect_uri: T, client_assertion: T, @@ -40,7 +41,6 @@ impl AuthorizationCodeCertificateCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_id: client_id.as_ref().to_owned(), - client_secret: client_secret.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), code_verifier: None, client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" @@ -91,13 +91,6 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { ); } - if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_msg( - OAuthCredential::ClientSecret.alias(), - None, - ); - } - if self.client_assertion.trim().is_empty() { return AuthorizationFailure::required_value_msg( OAuthCredential::ClientAssertion.alias(), @@ -112,7 +105,6 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { self.serializer .client_id(self.client_id.as_str()) - .client_secret(self.client_secret.as_str()) .redirect_uri(self.redirect_uri.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) @@ -189,7 +181,6 @@ impl AuthorizationCodeCertificateCredentialBuilder { authorization_code: None, refresh_token: None, client_id: String::new(), - client_secret: String::new(), redirect_uri: String::new(), code_verifier: None, client_assertion_type: String::new(), @@ -221,12 +212,6 @@ impl AuthorizationCodeCertificateCredentialBuilder { self.credential.client_id = client_id.as_ref().to_owned(); self } - - pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { - self.credential.client_secret = client_secret.as_ref().to_owned(); - self - } - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); @@ -243,6 +228,15 @@ impl AuthorizationCodeCertificateCredentialBuilder { self } + pub fn with_certificate( + &mut self, + certificate_assertion: &ClientAssertion, + ) -> anyhow::Result<&mut Self> { + self.with_client_assertion(certificate_assertion.sign()?); + self.with_client_assertion_type("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + Ok(self) + } + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { self.credential.client_assertion = client_assertion.as_ref().to_owned(); self diff --git a/graph-oauth/src/identity/credentials/client_assertion.rs b/graph-oauth/src/identity/credentials/client_assertion.rs index 1a57ca0b..964c0c84 100644 --- a/graph-oauth/src/identity/credentials/client_assertion.rs +++ b/graph-oauth/src/identity/credentials/client_assertion.rs @@ -1,97 +1,202 @@ -use std::collections::HashMap; -use std::fs::OpenOptions; -use std::io::Read; -use chrono::{DateTime, Utc}; -use openssl::hash::{DigestBytes, MessageDigest}; -use openssl::pkey::{PKey, Private}; -use openssl::x509; -use openssl::x509::X509; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use anyhow::Context; use base64::Engine; + use openssl::error::ErrorStack; -use openssl::rsa::{Padding, Rsa}; +use openssl::hash::MessageDigest; +use openssl::pkey::{PKey, Private}; + +use openssl::rsa::Padding; use openssl::sign::Signer; -pub struct SignedClientAssertion { +use openssl::x509::X509; + +use std::collections::HashMap; + +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +/// Computes the client assertion used in certificate credential authorization flows. +/// The client assertion is computed from the DER encoding of an X509 certificate and it's private key. +/// +/// Client assertions are generated using the openssl library for security reasons. +/// You can see an example of how this is done by Microsoft located at +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions +pub struct ClientAssertion { client_id: String, tenant_id: Option<String>, + claims: Option<HashMap<String, String>>, + extend_claims: bool, certificate: X509, - pkey: PKey<Private> + pkey: PKey<Private>, + uuid: Uuid, } -impl SignedClientAssertion { +impl ClientAssertion { pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { Self { client_id: client_id.as_ref().to_owned(), tenant_id: None, + claims: None, + extend_claims: true, certificate, - pkey: private_key + pkey: private_key, + uuid: Uuid::new_v4(), } } - pub fn new_with_tenant<T: AsRef<str>>(client_id: T, tenant_id: T, certificate: X509, private_key: PKey<Private>) -> SignedClientAssertion { + pub fn new_with_tenant<T: AsRef<str>>( + client_id: T, + tenant_id: T, + certificate: X509, + private_key: PKey<Private>, + ) -> ClientAssertion { Self { client_id: client_id.as_ref().to_owned(), tenant_id: Some(tenant_id.as_ref().to_owned()), + claims: None, + extend_claims: true, certificate, - pkey: private_key + pkey: private_key, + uuid: Uuid::new_v4(), } } - /// Base64url-encoded SHA-1 thumbprint of the X.509 certificate's DER encoding. + /// Provide your own set of claims in the payload of the JWT. + /// + /// Set extend_claims to false in order to replace the claims that would be generated + /// for the client assertion. This replaces the following payload fields: aud, exp, nbf, jti, + /// sub, and iss. This ensures that only the claims given are passed for the payload of the JWT + /// used in the client assertion. + /// + /// If extend claims is true, the claims provided are in addition + /// to those claims mentioned above and do not replace them, however, any claim provided + /// with the same fields above will replace those that are generated. + pub fn with_claims(&mut self, claims: HashMap<String, String>, extend_claims: bool) { + self.claims = Some(claims); + self.extend_claims = extend_claims; + } + + /// Hex encoded SHA-1 thumbprint of the X.509 certificate's DER encoding. + /// + /// You can verify that the correct certificate has been passed + /// by comparing the hex encoded thumbprint against the thumbprint given in Azure + /// Active Directory under Certificates and Secrets for your application or by looking + /// at the keyCredentials customKeyIdentifier field in your applications manifest. pub fn get_thumbprint(&self) -> Result<String, ErrorStack> { - let hash = self.certificate.digest(MessageDigest::sha1())?; - let hash_hex = hex::encode(hash.as_ref()); - println!("{hash_hex:#?}"); + let digest_bytes = self.certificate.digest(MessageDigest::sha1())?; + Ok(hex::encode(digest_bytes.as_ref()).to_uppercase()) + } + + /// Base64 Url encoded (No Pad) SHA-1 thumbprint of the X.509 certificate's DER encoding. + pub fn get_thumbprint_base64(&self) -> Result<String, ErrorStack> { + let digest_bytes = self.certificate.digest(MessageDigest::sha1())?; + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest_bytes)) + } - //println!("DigestBytes: {:#?}", std::str::from_utf8(hash.as_ref())); - Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash_hex)) + /// Get the value used for the jti field in the payload. This field is computed + /// when constructing the [ClientAssertion] and will be different from any + /// custom claims provided. + /// + /// The "jti" (JWT ID) claim provides a unique identifier for the JWT. + /// The identifier value MUST be assigned in a manner that ensures that there is + /// a negligible probability that the same value will be accidentally assigned to + /// a different data object; if the application uses multiple issuers, collisions + /// MUST be prevented among values produced by different issuers as well. + pub fn get_uuid(&self) -> &Uuid { + &self.uuid } - pub fn get_header(&self) -> Result<HashMap<String, String>, ErrorStack> { + fn get_header(&self) -> Result<HashMap<String, String>, ErrorStack> { let mut header = HashMap::new(); - header.insert("x5t".to_owned(), self.get_thumbprint()?); + header.insert("x5t".to_owned(), self.get_thumbprint_base64()?); header.insert("alg".to_owned(), "RS256".to_owned()); header.insert("typ".to_owned(), "JWT".to_owned()); Ok(header) } - pub fn get_claims(&self) -> HashMap<String, String> { + fn get_claims(&self) -> anyhow::Result<HashMap<String, String>> { + if let Some(claims) = self.claims.as_ref() { + if !self.extend_claims { + return Ok(claims.clone()); + } + } + let aud = { - if let Some(tenant_id) = self.tenant_id.as_ref() { - format!("https://login.microsoftonline.com/{}/v2.0", tenant_id) - } else { - "https://login.microsoftonline.com/common/v2.0".to_owned() - } + if let Some(tenant_id) = self.tenant_id.as_ref() { + format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + tenant_id + ) + } else { + "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_owned() + } }; // 10 minutes until expiration. let exp = 60 * 10; - let nbf = SystemTime::now().duration_since(UNIX_EPOCH).unwrap(); - let exp = nbf.checked_add(Duration::from_secs(exp)).unwrap(); + let nbf = SystemTime::now().duration_since(UNIX_EPOCH)?; + let exp = nbf + .checked_add(Duration::from_secs(exp)) + .context("Unable to set exp claims field - Reason: Unknown")?; let mut claims = HashMap::new(); claims.insert("aud".to_owned(), aud); claims.insert("exp".to_owned(), exp.as_secs().to_string()); claims.insert("nbf".to_owned(), nbf.as_secs().to_string()); + claims.insert("jti".to_owned(), Uuid::new_v4().to_string()); claims.insert("sub".to_owned(), self.client_id.to_owned()); claims.insert("iss".to_owned(), self.client_id.to_owned()); - claims + + if let Some(internal_claims) = self.claims.as_ref() { + claims.extend(internal_claims.clone()); + } + + Ok(claims) } - pub fn get_signed_client_assertion(&self) -> Result<String, ErrorStack> { + /// JWT Header and Payload in the format header.payload + fn base64_token(&self) -> anyhow::Result<String> { let header = self.get_header()?; - let header_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap()); - let claims = self.get_claims(); - let claims_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims).unwrap()); - let token = format!("{}.{}", header_base64, claims_base64); + let header = serde_json::to_string(&header)?; + let header_base64 = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes()); + let claims = self.get_claims()?; + let claims = serde_json::to_string(&claims)?; + let claims_base64 = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes()); + Ok(format!("{}.{}", header_base64, claims_base64)) + } + + /* + Altogether the general flow is as follows: + let header = self.get_header()?; + let header = serde_json::to_string(&header).unwrap(); + let header_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes()); + let claims = self.get_claims(); + let claims = serde_json::to_string(&claims).unwrap(); + let claims_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes()); + let token = format!("{}.{}", header_base64, claims_base64); + + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; + signer.set_rsa_padding(Padding::PKCS1)?; + signer.update(token.as_str().as_bytes())?; + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + let signed_client_assertion = format!("{token}.{signature}"); + Ok(signed_client_assertion) + */ + + /// Get the signed client assertion. + /// + /// The signature is a Base64 Url encoded (No Pad) JWT Header and Payload signed with the private key using SHA_256 + /// and RSA padding PKCS1 + pub fn sign(&self) -> anyhow::Result<String> { + let token = self.base64_token()?; let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; signer.set_rsa_padding(Padding::PKCS1)?; signer.update(token.as_str().as_bytes())?; - let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); - let signed_client_assertion = format!("{token}.{signature}"); - - Ok(signed_client_assertion) + let signature = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + Ok(format!("{token}.{signature}")) } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index cba74b79..a6b79557 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,5 +1,9 @@ use crate::auth::OAuth; -use crate::identity::Authority; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, ClientAssertion}; +use graph_error::{AuthorizationResult, GraphResult}; + +use std::collections::HashMap; +use url::Url; #[derive(Clone)] #[allow(dead_code)] @@ -13,6 +17,8 @@ pub struct ClientCertificateCredential { /// Default is https://graph.microsoft.com/.default. pub(crate) scopes: Vec<String>, pub(crate) authority: Authority, + pub(crate) client_assertion_type: String, + pub(crate) client_assertion: String, serializer: OAuth, } @@ -22,6 +28,16 @@ impl ClientCertificateCredential { } } +impl AuthorizationSerializer for ClientCertificateCredential { + fn uri(&mut self, _azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + unimplemented!() + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + unimplemented!() + } +} + pub struct ClientCertificateCredentialBuilder { credential: ClientCertificateCredential, } @@ -34,6 +50,9 @@ impl ClientCertificateCredentialBuilder { certificate: String::new(), scopes: vec![], authority: Default::default(), + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + .to_owned(), + client_assertion: String::new(), serializer: OAuth::new(), }, } @@ -44,8 +63,25 @@ impl ClientCertificateCredentialBuilder { self } - pub fn with_certificate<T: AsRef<str>>(&mut self, certificate: T) -> &mut Self { - self.credential.certificate = certificate.as_ref().to_owned(); + pub fn with_certificate( + &mut self, + certificate_assertion: &ClientAssertion, + ) -> anyhow::Result<&mut Self> { + self.with_client_assertion(certificate_assertion.sign()?); + self.with_client_assertion_type("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + Ok(self) + } + + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + self.credential.client_assertion = client_assertion.as_ref().to_owned(); + self + } + + pub fn with_client_assertion_type<T: AsRef<str>>( + &mut self, + client_assertion_type: T, + ) -> &mut Self { + self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); self } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 62b0a6ff..34be3ec2 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,6 +1,6 @@ use crate::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - ClientSecretCredential, TokenCredentialOptions, TokenRequest, + ClientCertificateCredential, ClientSecretCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::GraphResult; @@ -28,7 +28,7 @@ impl ConfidentialClientApplication { #[async_trait] impl TokenRequest for ConfidentialClientApplication { - fn get_token_silent(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { let uri = self .credential .uri(&self.token_credential_options.azure_authority_host)?; @@ -37,7 +37,7 @@ impl TokenRequest for ConfidentialClientApplication { Ok(http_client.post(uri).form(&form).send()?) } - async fn get_token_silent_async(&mut self) -> anyhow::Result<Response> { + async fn get_token_async(&mut self) -> anyhow::Result<Response> { let uri = self .credential .uri(&self.token_credential_options.azure_authority_host)?; @@ -76,6 +76,16 @@ impl From<ClientSecretCredential> for ConfidentialClientApplication { } } +impl From<ClientCertificateCredential> for ConfidentialClientApplication { + fn from(value: ClientCertificateCredential) -> Self { + ConfidentialClientApplication { + http_client: reqwest::Client::new(), + credential: Box::new(value), + token_credential_options: Default::default(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index b06ddcf9..229da8fa 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,6 +1,7 @@ mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; +mod client_assertion; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; @@ -14,6 +15,7 @@ mod token_request; pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; +pub use client_assertion::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 799979e9..c88d62ec 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -2,6 +2,6 @@ use async_trait::async_trait; #[async_trait] pub trait TokenRequest { - fn get_token_silent(&mut self) -> anyhow::Result<reqwest::blocking::Response>; - async fn get_token_silent_async(&mut self) -> anyhow::Result<reqwest::Response>; + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response>; + async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response>; } diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 7e09eb62..cf5fd5a1 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -6,3 +6,6 @@ mod serialize; pub use authority::*; pub use credentials::*; pub use serialize::*; + +pub use openssl::pkey::{PKey, Private}; +pub use openssl::x509::X509; From e47ee283ba6664b809176bae551488f48c9f0a84 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 23 Apr 2023 17:03:40 -0400 Subject: [PATCH 007/118] Put openssl behind feature flag --- Cargo.toml | 13 ++++-- examples/oauth/main.rs | 45 ++++++++++--------- .../main.rs} | 27 +++++++++-- graph-http/Cargo.toml | 3 ++ graph-oauth/Cargo.toml | 6 ++- ...thorization_code_certificate_credential.rs | 6 ++- .../client_certificate_credential.rs | 7 ++- graph-oauth/src/identity/credentials/mod.rs | 8 +++- graph-oauth/src/identity/mod.rs | 7 ++- 9 files changed, 86 insertions(+), 36 deletions(-) rename examples/{oauth/auth_code_grant_certificate.rs => oauth_certificate/main.rs} (88%) diff --git a/Cargo.toml b/Cargo.toml index fe4cf9ae..f5b56f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,10 @@ graph-core = { path = "./graph-core", version = "0.4.0" } default = ["native-tls"] native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls"] rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls"] -brotli = ["reqwest/brotli"] -deflate = ["reqwest/deflate"] -trust-dns = ["reqwest/trust-dns"] +brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli"] +deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate"] +trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns"] +openssl = ["graph-oauth/openssl"] [dev-dependencies] bytes = { version = "1.4.0" } @@ -63,3 +64,9 @@ test-tools = { path = "./test-tools", version = "0.0.1" } [profile.release] debug = false + +[[example]] +name = "oauth_certificate_main" +path = "examples/oauth_certificate/main.rs" +required-features = ["openssl"] + diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index cda0709b..25c56ee7 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -16,7 +16,6 @@ extern crate serde; mod auth_code_grant; -mod auth_code_grant_certificate; mod auth_code_grant_pkce; mod client_credentials; mod client_credentials_admin_consent; @@ -52,35 +51,41 @@ async fn main() { open_id_connect::start_server_main().await; } -// Examples +// Quick Examples // Authorization Code Grant -fn auth_code_grant(authorization_code: &str) -> AuthorizationCodeCredential { +async fn auth_code_grant(authorization_code: &str) { let pkce = ProofKeyForCodeExchange::generate().unwrap(); - AuthorizationCodeCredential::builder() + let credential = AuthorizationCodeCredential::builder() .with_authorization_code(authorization_code) .with_client_id("CLIENT_ID") .with_client_secret("CLIENT_SECRET") .with_redirect_uri("http://localhost:8000/redirect") .with_proof_key_for_code_exchange(&pkce) - .build() + .build(); + + let mut confidential_client = ConfidentialClientApplication::from(credential); + + let response = confidential_client.get_token_async().await.unwrap(); + println!("{response:#?}"); + + let access_token: AccessToken = response.json().await.unwrap(); + println!("{:#?}", access_token.bearer_token()); } // Client Credentials Grant -fn client_credentials() { - pub async fn get_token_silent() { - let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); - let mut confidential_client_application = - ConfidentialClientApplication::from(client_secret_credential); - - let response = confidential_client_application - .get_token_async() - .await - .unwrap(); - println!("{response:#?}"); - - let access_token: AccessToken = response.json().await.unwrap(); - println!("{:#?}", access_token.bearer_token()); - } +async fn client_credentials() { + let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); + let mut confidential_client_application = + ConfidentialClientApplication::from(client_secret_credential); + + let response = confidential_client_application + .get_token_async() + .await + .unwrap(); + println!("{response:#?}"); + + let access_token: AccessToken = response.json().await.unwrap(); + println!("{:#?}", access_token.bearer_token()); } diff --git a/examples/oauth/auth_code_grant_certificate.rs b/examples/oauth_certificate/main.rs similarity index 88% rename from examples/oauth/auth_code_grant_certificate.rs rename to examples/oauth_certificate/main.rs index 2e3e8d2b..7813d6e6 100644 --- a/examples/oauth/auth_code_grant_certificate.rs +++ b/examples/oauth_certificate/main.rs @@ -1,11 +1,27 @@ +#![allow(dead_code)] + +#[macro_use] +extern crate serde; + use graph_rs_sdk::oauth::{ AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCertificateCredential, ClientAssertion, - ConfidentialClientApplication, PKey, Private, TokenRequest, X509, + ConfidentialClientApplication, PKey, TokenRequest, X509, }; use std::fs::File; use std::io::Read; use warp::Filter; +#[tokio::main] +async fn main() { + start_server_main().await; +} + +// X509 certificates can be used for the auth code grant with +// a certificate (AuthorizationCodeCertificateCredential) and +// the client credentials grant with a certificate (ClientCertificateCredential). + +// The example below shows using the authorization code grant with a certificate. + // This flow uses an X509 certificate for authorization. The public key should // be uploaded to Azure Active Directory. In order to use the certificate // flow the ClientAssertion struct can be used to generate the needed @@ -58,11 +74,11 @@ pub fn get_confidential_client( ) -> anyhow::Result<ConfidentialClientApplication> { let mut cert_file = File::open(PRIVATE_KEY_PATH).unwrap(); let mut certificate: Vec<u8> = Vec::new(); - cert_file.read_to_end(&mut cert); + cert_file.read_to_end(&mut certificate)?; let mut private_key_file = File::open(CERTIFICATE_PATH).unwrap(); let mut private_key: Vec<u8> = Vec::new(); - private_key_file.read_to_end(&mut cert); + private_key_file.read_to_end(&mut private_key)?; let cert = X509::from_pem(certificate.as_slice()).unwrap(); let pkey = PKey::private_key_from_pem(private_key.as_slice()).unwrap(); @@ -136,7 +152,10 @@ pub async fn start_server_main() { .map(Some) .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - let routes = warp::get().and(query).and_then(handle_redirect); + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); authorization_sign_in(CLIENT_ID, TENANT); diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 5d4b7679..2336f1c7 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -30,3 +30,6 @@ graph-core = { path = "../graph-core" } default = ["native-tls"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 0547565d..4d1edd03 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -18,7 +18,7 @@ base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" hex = "0.4.3" -openssl = "0.10" +openssl = { version = "0.10", optional=true } reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" serde = { version = "1", features = ["derive"] } @@ -35,3 +35,7 @@ graph-error = { path = "../graph-error" } default = ["native-tls"] native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] +openssl = ["dep:openssl"] diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 9db575b9..e6d69d95 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -4,12 +4,13 @@ use crate::identity::form_credential::FormCredential; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, }; -use crate::oauth::ClientAssertion; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; - use std::collections::HashMap; use url::Url; +#[cfg(feature = "openssl")] +use crate::oauth::ClientAssertion; + #[derive(Clone)] pub struct AuthorizationCodeCertificateCredential { /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. @@ -228,6 +229,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { self } + #[cfg(feature = "openssl")] pub fn with_certificate( &mut self, certificate_assertion: &ClientAssertion, diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index a6b79557..54272b92 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,10 +1,12 @@ use crate::auth::OAuth; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, ClientAssertion}; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; use graph_error::{AuthorizationResult, GraphResult}; - use std::collections::HashMap; use url::Url; +#[cfg(feature = "openssl")] +use crate::identity::ClientAssertion; + #[derive(Clone)] #[allow(dead_code)] pub struct ClientCertificateCredential { @@ -63,6 +65,7 @@ impl ClientCertificateCredentialBuilder { self } + #[cfg(feature = "openssl")] pub fn with_certificate( &mut self, certificate_assertion: &ClientAssertion, diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 229da8fa..c6dce850 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,7 +1,6 @@ mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; -mod client_assertion; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; @@ -12,10 +11,12 @@ mod response_mode; mod token_credential; mod token_request; +#[cfg(feature = "openssl")] +mod client_assertion; + pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; -pub use client_assertion::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; @@ -25,3 +26,6 @@ pub use proof_key_for_code_exchange::*; pub use response_mode::*; pub use token_credential::*; pub use token_request::*; + +#[cfg(feature = "openssl")] +pub use client_assertion::*; diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index cf5fd5a1..a932d82b 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -7,5 +7,8 @@ pub use authority::*; pub use credentials::*; pub use serialize::*; -pub use openssl::pkey::{PKey, Private}; -pub use openssl::x509::X509; +#[cfg(feature = "openssl")] +pub use openssl::{ + pkey::{PKey, Private}, + x509::X509, +}; From ace965188dbab0da43c437894d1ed59e6f8224e5 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 23 Apr 2023 21:19:05 -0400 Subject: [PATCH 008/118] Add implicit credential authorization url and ResponseType enum --- .../oauth/auth_code_grant_refresh_token.rs | 36 ++ examples/oauth/implicit_grant.rs | 89 ++-- examples/oauth/main.rs | 9 +- examples/oauth_certificate/main.rs | 2 + .../auth_code_authorization_url.rs | 50 +- ...thorization_code_certificate_credential.rs | 55 ++- .../authorization_code_credential.rs | 46 +- .../identity/credentials/client_assertion.rs | 5 - .../implicit_credential_authorization_url.rs | 445 ++++++++++++++++++ graph-oauth/src/identity/credentials/mod.rs | 10 + .../credentials/public_client_application.rs | 1 + .../src/identity/credentials/response_type.rs | 34 ++ 12 files changed, 702 insertions(+), 80 deletions(-) create mode 100644 examples/oauth/auth_code_grant_refresh_token.rs create mode 100644 graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs create mode 100644 graph-oauth/src/identity/credentials/public_client_application.rs create mode 100644 graph-oauth/src/identity/credentials/response_type.rs diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant_refresh_token.rs new file mode 100644 index 00000000..9d3ffd5f --- /dev/null +++ b/examples/oauth/auth_code_grant_refresh_token.rs @@ -0,0 +1,36 @@ +use graph_oauth::identity::AuthorizationCodeCredentialBuilder; +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ConfidentialClientApplication, TokenRequest, +}; + +// Use a refresh token to get a new access token. + +async fn using_auth_code_credential( + credential: &mut AuthorizationCodeCredential, + refresh_token: &str, +) { + credential.with_refresh_token(refresh_token); + + let _response = credential.get_token_async().await; +} + +async fn using_confidential_client( + mut credential: AuthorizationCodeCredential, + refresh_token: &str, +) { + credential.with_refresh_token(refresh_token); + let mut confidential_client = ConfidentialClientApplication::from(credential); + + let _response = confidential_client.get_token_async().await; +} + +async fn using_auth_code_credential_builder( + credential: AuthorizationCodeCredential, + refresh_token: &str, +) { + let mut credential = AuthorizationCodeCredentialBuilder::from(credential) + .with_refresh_token(refresh_token) + .build(); + + let _response = credential.get_token_async().await; +} diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index d9cefa38..5b632b40 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -1,46 +1,57 @@ -/// The following example shows authenticating an application to use the OneDrive REST API -/// for a native client. Native clients typically use the implicit OAuth flow. This requires -/// using the browser to log in. To get an access token, set the response type to 'token' -/// which will return an access token in the URL. The implicit flow does not make POST requests -/// for access tokens like other flows do. -/// -/// There are two versions of the implicit flow. The first, called token flow is used -/// for Microsoft V1.0 OneDrive authentication. The second is Microsoft's implementation -/// of the OAuth V2.0 implicit flow. -/// -/// Implicit flows are typically performed when requesting access tokens directly from -/// the user agent such as from a browser using JavaScript. -/// -/// For more information on the implicit flows see: -/// 1. Token flow for v1.0: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online -/// 2. Implicit grant flow for v2.0: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow -/// -/// To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 -use graph_rs_sdk::oauth::OAuth; +// The following example shows authenticating an application to use the OneDrive REST API +// for a native client. Native clients typically use the implicit OAuth flow. This requires +// using the browser to log in. To get an access token, set the response type to 'token' +// which will return an access token in the URL. The implicit flow does not make POST requests +// for access tokens like other flows do. +// +// There are two versions of the implicit flow. The first, called token flow is used +// for Microsoft V1.0 OneDrive authentication. The second is Microsoft's implementation +// of the OAuth V2.0 implicit flow. +// +// Implicit flows are typically performed when requesting access tokens directly from +// the user agent such as from a browser using JavaScript. +// +// For more information on the implicit flows see: +// 1. Token flow for v1.0: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online +// 2. Implicit grant flow for v2.0: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow +// +// To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 +use graph_rs_sdk::oauth::{ImplicitCredentialAuthorizationUrl, Prompt, ResponseMode, ResponseType}; -fn oauth_implicit_flow() -> OAuth { - let mut oauth = OAuth::new(); - oauth - .client_id("<YOUR_CLIENT_ID>") - .redirect_uri("http://localhost:8000/redirect") - .add_scope("Files.Read") - .add_scope("Files.ReadWrite") - .add_scope("Files.Read.All") - .add_scope("Files.ReadWrite.All") - .response_type("token") - .response_mode("query") - .prompt("login") - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .access_token_url("https://login.live.com/oauth20_token.srf"); - oauth -} +fn oauth_implicit_flow() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("<YOUR_CLIENT_ID>") + .with_redirect_uri("http://localhost:8000/redirect") + .with_prompt(Prompt::Login) + .with_response_type(ResponseType::Token) + .with_response_mode(ResponseMode::Fragment) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url = authorizer.url().unwrap(); + + // webbrowser crate in dev dependencies will open to default browser in the system. -fn request_token_main() { // Opens the default browser to the Microsoft login page. // After logging in the page will redirect and the Url // will have the access token in either the query or // the fragment of the Uri. - let mut oauth = oauth_implicit_flow(); - let mut request = oauth.build().implicit_grant(); - request.browser_authorization().open().unwrap(); + webbrowser::open(url.as_str()).unwrap(); +} + +fn multi_response_types() { + let _ = ImplicitCredentialAuthorizationUrl::builder() + .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) + .build(); + + // Or + + let _ = ImplicitCredentialAuthorizationUrl::builder() + .with_response_type(ResponseType::FromString(vec![ + "token".to_string(), + "id_token".to_string(), + ])) + .build(); } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 25c56ee7..90c19486 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -17,6 +17,7 @@ extern crate serde; mod auth_code_grant; mod auth_code_grant_pkce; +mod auth_code_grant_refresh_token; mod client_credentials; mod client_credentials_admin_consent; mod code_flow; @@ -77,13 +78,9 @@ async fn auth_code_grant(authorization_code: &str) { // Client Credentials Grant async fn client_credentials() { let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); - let mut confidential_client_application = - ConfidentialClientApplication::from(client_secret_credential); + let mut confidential_client = ConfidentialClientApplication::from(client_secret_credential); - let response = confidential_client_application - .get_token_async() - .await - .unwrap(); + let response = confidential_client.get_token_async().await.unwrap(); println!("{response:#?}"); let access_token: AccessToken = response.json().await.unwrap(); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 7813d6e6..c5609ffe 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -16,6 +16,8 @@ async fn main() { start_server_main().await; } +// Requires feature openssl be enabled for graph-rs-sdk or graph-oauth + // X509 certificates can be used for the auth code grant with // a certificate (AuthorizationCodeCertificateCredential) and // the client credentials grant with a certificate (ClientCertificateCredential). diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 878fe0ed..bf0171a2 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::grants::GrantType; use crate::identity::{Authority, AzureAuthorityHost, Prompt, ResponseMode}; use crate::oauth::form_credential::FormCredential; -use crate::oauth::ProofKeyForCodeExchange; +use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -26,7 +26,7 @@ pub struct AuthCodeAuthorizationUrl { pub(crate) client_id: String, pub(crate) redirect_uri: String, pub(crate) authority: Authority, - pub(crate) response_type: String, + pub(crate) response_type: Vec<ResponseType>, pub(crate) response_mode: Option<ResponseMode>, pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, @@ -44,7 +44,7 @@ impl AuthCodeAuthorizationUrl { client_id: client_id.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), authority: Authority::default(), - response_type: "code".to_owned(), + response_type: vec![ResponseType::Code], response_mode: None, nonce: None, state: None, @@ -83,13 +83,6 @@ impl AuthCodeAuthorizationUrl { return AuthorizationFailure::required_value_msg("client_id", None); } - if self.response_type.trim().is_empty() { - return AuthorizationFailure::required_value_msg( - "response_type", - Some("Must include code for the authorization code flow. Can also include id_token or token if using the hybrid flow.") - ); - } - if self.scope.is_empty() { return AuthorizationFailure::required_value_msg("scope", None); } @@ -98,8 +91,32 @@ impl AuthCodeAuthorizationUrl { .client_id(self.client_id.as_str()) .redirect_uri(self.redirect_uri.as_str()) .extend_scopes(self.scope.clone()) - .authority(azure_authority_host, &self.authority) - .response_type(self.response_type.as_str()); + .authority(azure_authority_host, &self.authority); + + let response_types: Vec<String> = + self.response_type.iter().map(|s| s.to_string()).collect(); + + if response_types.is_empty() { + serializer.response_type("code"); + if let Some(response_mode) = self.response_mode.as_ref() { + serializer.response_mode(response_mode.as_ref()); + } + } else { + let response_type_string = response_types.join(" "); + let mut response_type = response_type_string.trim(); + if response_type.is_empty() { + serializer.response_type("code"); + response_type = "code"; + } else { + serializer.response_type(response_type); + } + + if response_type.contains("id_token") { + serializer.response_mode(ResponseMode::Fragment.as_ref()); + } else if let Some(response_mode) = self.response_mode.as_ref() { + serializer.response_mode(response_mode.as_ref()); + } + } if let Some(response_mode) = self.response_mode.as_ref() { serializer.response_mode(response_mode.as_ref()); @@ -175,7 +192,7 @@ impl AuthCodeAuthorizationUrlBuilder { redirect_uri: String::new(), authority: Authority::default(), response_mode: None, - response_type: "code".to_owned(), + response_type: vec![ResponseType::Code], nonce: None, state: None, scope: vec![], @@ -212,8 +229,11 @@ impl AuthCodeAuthorizationUrlBuilder { /// Default is code. Must include code for the authorization code flow. /// Can also include id_token or token if using the hybrid flow. - pub fn with_response_type<T: AsRef<str>>(&mut self, response_type: T) -> &mut Self { - self.authorization_code_authorize_url.response_type = response_type.as_ref().to_owned(); + pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( + &mut self, + response_type: I, + ) -> &mut Self { + self.authorization_code_authorize_url.response_type = response_type.into_iter().collect(); self } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index e6d69d95..3ddf1ef3 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,10 +1,12 @@ use crate::auth::{OAuth, OAuthCredential}; -use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, + TokenCredentialOptions, TokenRequest, }; +use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use reqwest::Response; use std::collections::HashMap; use url::Url; @@ -24,9 +26,10 @@ pub struct AuthorizationCodeCertificateCredential { pub(crate) code_verifier: Option<String>, pub(crate) client_assertion_type: String, pub(crate) client_assertion: String, - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, + pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, } @@ -47,21 +50,37 @@ impl AuthorizationCodeCertificateCredential { client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" .to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - scopes: vec![], + scope: vec![], authority: Default::default(), + token_credential_options: TokenCredentialOptions::default(), serializer: OAuth::new(), } } - pub fn grant_type(&self) -> GrantType { - GrantType::AuthorizationCode - } - pub fn builder() -> AuthorizationCodeCertificateCredentialBuilder { AuthorizationCodeCertificateCredentialBuilder::new() } } +#[async_trait] +impl TokenRequest for AuthorizationCodeCertificateCredential { + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::blocking::Client::new(); + Ok(http_client.post(uri).form(&form).send()?) + } + + async fn get_token_async(&mut self) -> anyhow::Result<Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::Client::new(); + Ok(http_client.post(uri).form(&form).send().await?) + } +} + impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { self.serializer @@ -109,7 +128,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { .redirect_uri(self.redirect_uri.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) - .extend_scopes(self.scopes.clone()); + .extend_scopes(self.scope.clone()); if let Some(code_verifier) = self.code_verifier.as_ref() { self.serializer.code_verifier(code_verifier.as_ref()); @@ -186,8 +205,9 @@ impl AuthorizationCodeCertificateCredentialBuilder { code_verifier: None, client_assertion_type: String::new(), client_assertion: String::new(), - scopes: vec![], + scope: vec![], authority: Default::default(), + token_credential_options: TokenCredentialOptions::default(), serializer: OAuth::new(), }, } @@ -253,10 +273,17 @@ impl AuthorizationCodeCertificateCredentialBuilder { } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + self.credential.scope = scopes.into_iter().map(|s| s.to_string()).collect(); self } + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } + pub fn build(&self) -> AuthorizationCodeCertificateCredential { self.credential.clone() } @@ -274,3 +301,11 @@ impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBu builder } } + +impl From<AuthorizationCodeCertificateCredential> + for AuthorizationCodeCertificateCredentialBuilder +{ + fn from(credential: AuthorizationCodeCertificateCredential) -> Self { + AuthorizationCodeCertificateCredentialBuilder { credential } + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 75c92d47..2d0f3e0c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,13 +1,13 @@ use crate::auth::{OAuth, OAuthCredential}; -use crate::grants::GrantType; use crate::identity::form_credential::FormCredential; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, - ProofKeyForCodeExchange, + ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, }; +use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use reqwest::Response; use std::collections::HashMap; - use url::Url; #[derive(Clone)] @@ -25,6 +25,7 @@ pub struct AuthorizationCodeCredential { /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, pub(crate) code_verifier: Option<String>, + pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, } @@ -44,12 +45,14 @@ impl AuthorizationCodeCredential { scopes: vec![], authority: Default::default(), code_verifier: None, + token_credential_options: TokenCredentialOptions::default(), serializer: OAuth::new(), } } - pub fn grant_type(&self) -> GrantType { - GrantType::AuthorizationCode + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { + self.authorization_code = None; + self.refresh_token = Some(refresh_token.as_ref().to_owned()); } pub fn builder() -> AuthorizationCodeCredentialBuilder { @@ -168,6 +171,25 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } } +#[async_trait] +impl TokenRequest for AuthorizationCodeCredential { + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::blocking::Client::new(); + Ok(http_client.post(uri).form(&form).send()?) + } + + async fn get_token_async(&mut self) -> anyhow::Result<Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::Client::new(); + Ok(http_client.post(uri).form(&form).send().await?) + } +} + #[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { credential: AuthorizationCodeCredential, @@ -185,6 +207,7 @@ impl AuthorizationCodeCredentialBuilder { scopes: vec![], authority: Default::default(), code_verifier: None, + token_credential_options: TokenCredentialOptions::default(), serializer: OAuth::new(), }, } @@ -245,6 +268,13 @@ impl AuthorizationCodeCredentialBuilder { self } + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } + pub fn build(&self) -> AuthorizationCodeCredential { self.credential.clone() } @@ -263,6 +293,12 @@ impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { } } +impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { + fn from(credential: AuthorizationCodeCredential) -> Self { + AuthorizationCodeCredentialBuilder { credential } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/client_assertion.rs b/graph-oauth/src/identity/credentials/client_assertion.rs index 964c0c84..5e35efbc 100644 --- a/graph-oauth/src/identity/credentials/client_assertion.rs +++ b/graph-oauth/src/identity/credentials/client_assertion.rs @@ -1,17 +1,12 @@ use anyhow::Context; use base64::Engine; - use openssl::error::ErrorStack; use openssl::hash::MessageDigest; use openssl::pkey::{PKey, Private}; - use openssl::rsa::Padding; use openssl::sign::Signer; - use openssl::x509::X509; - use std::collections::HashMap; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; use uuid::Uuid; diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs new file mode 100644 index 00000000..045b43f8 --- /dev/null +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -0,0 +1,445 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::form_credential::FormCredential; +use crate::identity::{Authority, AzureAuthorityHost, ResponseMode}; +use crate::oauth::{Prompt, ResponseType}; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use url::form_urlencoded::Serializer; +use url::Url; +/// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens) +/// are returned directly from the /authorize endpoint instead of the /token endpoint. This is +/// often used as part of the authorization code flow, in what is called the "hybrid flow" - +/// retrieving the ID token on the /authorize request along with an authorization code. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow +#[derive(Clone)] +pub struct ImplicitCredentialAuthorizationUrl { + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required + /// If not set, defaults to code + /// Must include id_token for OpenID Connect sign-in. It may also include the response_type + /// token. Using token here will allow your app to receive an access token immediately from + /// the authorize endpoint without having to make a second request to the authorize endpoint. + /// If you use the token response_type, the scope parameter must contain a scope indicating + /// which resource to issue the token for (for example, user.read on Microsoft Graph). It can + /// also contain code in place of token to provide an authorization code, for use in the + /// authorization code flow. This id_token+code response is sometimes called the hybrid flow. + pub(crate) response_type: Vec<ResponseType>, + /// Optional + /// The redirect_uri of your app, where authentication responses can be sent and received + /// by your app. It must exactly match one of the redirect_uris you registered in the portal, + /// except it must be URL-encoded. + pub(crate) redirect_uri: Option<String>, + /// Required + /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the + /// scope openid, which translates to the "Sign you in" permission in the consent UI. + /// Optionally you may also want to include the email and profile scopes for gaining access + /// to additional user data. You may also include other scopes in this request for requesting + /// consent to various resources, if an access token is requested. + pub(crate) scope: Vec<String>, + /// Optional + /// Specifies the method that should be used to send the resulting token back to your app. + /// Defaults to query for just an access token, but fragment if the request includes an id_token. + pub(crate) response_mode: ResponseMode, + /// Optional + /// A value included in the request that will also be returned in the token response. + /// It can be a string of any content that you wish. A randomly generated unique value is + /// typically used for preventing cross-site request forgery attacks. The state is also used + /// to encode information about the user's state in the app before the authentication request + /// occurred, such as the page or view they were on. + pub(crate) state: Option<String>, + /// Required + /// A value included in the request, generated by the app, that will be included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token replay + /// attacks. The value is typically a randomized, unique string that can be used to identify + /// the origin of the request. Only required when an id_token is requested. + pub(crate) nonce: String, + /// Optional + /// Indicates the type of user interaction that is required. The only valid values at this + /// time are 'login', 'none', 'select_account', and 'consent'. prompt=login will force the + /// user to enter their credentials on that request, negating single-sign on. prompt=none is + /// the opposite - it will ensure that the user isn't presented with any interactive prompt + /// whatsoever. If the request can't be completed silently via single-sign on, the Microsoft + /// identity platform will return an error. prompt=select_account sends the user to an account + /// picker where all of the accounts remembered in the session will appear. prompt=consent + /// will trigger the OAuth consent dialog after the user signs in, asking the user to grant + /// permissions to the app. + pub(crate) prompt: Option<Prompt>, + /// Optional + /// You can use this parameter to pre-fill the username and email address field of the sign-in + /// page for the user, if you know the username ahead of time. Often, apps use this parameter + /// during re-authentication, after already extracting the login_hint optional claim from an + /// earlier sign-in. + pub(crate) login_hint: Option<String>, + /// Optional + /// If included, it will skip the email-based discovery process that user goes through on + /// the sign-in page, leading to a slightly more streamlined user experience. This parameter + /// is commonly used for Line of Business apps that operate in a single tenant, where they'll + /// provide a domain name within a given tenant, forwarding the user to the federation provider + /// for that tenant. This hint prevents guests from signing into this application, and limits + /// the use of cloud credentials like FIDO. + pub(crate) domain_hint: Option<String>, + /// The Azure Active Directory tenant (directory) Id of the service principal. + pub(crate) authority: Authority, +} + +impl ImplicitCredentialAuthorizationUrl { + pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + client_id: T, + nonce: T, + scope: I, + ) -> ImplicitCredentialAuthorizationUrl { + ImplicitCredentialAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + response_type: vec![ResponseType::Code], + redirect_uri: None, + scope: scope.into_iter().map(|s| s.to_string()).collect(), + response_mode: ResponseMode::Query, + state: None, + nonce: nonce.as_ref().to_owned(), + prompt: None, + login_hint: None, + domain_hint: None, + authority: Default::default(), + } + } + + pub fn builder() -> ImplicitCredentialAuthorizationUrlBuilder { + ImplicitCredentialAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.url_with_host(&AzureAuthorityHost::default()) + } + + pub fn url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { + let mut serializer = OAuth::new(); + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_msg("client_id", None); + } + + if self.nonce.trim().is_empty() { + return AuthorizationFailure::required_value("nonce"); + } + + serializer + .client_id(self.client_id.as_str()) + .nonce(self.nonce.as_str()) + .extend_scopes(self.scope.clone()) + .authority(azure_authority_host, &self.authority); + + let response_types: Vec<String> = + self.response_type.iter().map(|rt| rt.to_string()).collect(); + + let mut response_type = response_types.join(" "); + + if response_type.trim().is_empty() { + serializer.response_type("code"); + response_type = "code".to_owned(); + } else { + serializer.response_type(response_type.as_str()); + } + + if response_type.contains("id_token") { + serializer.response_mode(ResponseMode::Fragment.as_ref()); + } else { + serializer.response_mode(self.response_mode.as_ref()); + } + + if self.scope.is_empty() { + if response_type.contains("id_token") { + serializer.add_scope("openid"); + } else { + return AuthorizationFailure::required_value_msg("scope", None); + } + } + + if let Some(state) = self.state.as_ref() { + serializer.state(state.as_str()); + } + + if let Some(prompt) = self.prompt.as_ref() { + serializer.prompt(prompt.as_ref()); + } + + if let Some(domain_hint) = self.domain_hint.as_ref() { + serializer.domain_hint(domain_hint.as_str()); + } + + if let Some(login_hint) = self.login_hint.as_ref() { + serializer.login_hint(login_hint.as_str()); + } + + let authorization_credentials = vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ResponseType), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::Nonce), + FormCredential::NotRequired(OAuthCredential::RedirectUri), + FormCredential::NotRequired(OAuthCredential::ResponseMode), + FormCredential::NotRequired(OAuthCredential::State), + FormCredential::NotRequired(OAuthCredential::Prompt), + FormCredential::NotRequired(OAuthCredential::LoginHint), + FormCredential::NotRequired(OAuthCredential::DomainHint), + ]; + + let mut encoder = Serializer::new(String::new()); + serializer.url_query_encode(authorization_credentials, &mut encoder)?; + + if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } else { + AuthorizationFailure::required_value_msg("authorization_url", Some("Internal Error")) + } + } +} + +#[derive(Clone)] +pub struct ImplicitCredentialAuthorizationUrlBuilder { + implicit_credential_authorization_url: ImplicitCredentialAuthorizationUrl, +} + +impl Default for ImplicitCredentialAuthorizationUrlBuilder { + fn default() -> Self { + Self::new() + } +} + +impl ImplicitCredentialAuthorizationUrlBuilder { + pub fn new() -> ImplicitCredentialAuthorizationUrlBuilder { + ImplicitCredentialAuthorizationUrlBuilder { + implicit_credential_authorization_url: ImplicitCredentialAuthorizationUrl { + client_id: String::new(), + response_type: vec![ResponseType::Code], + redirect_uri: None, + scope: vec![], + response_mode: ResponseMode::Query, + state: None, + nonce: String::new(), + prompt: None, + login_hint: None, + domain_hint: None, + authority: Default::default(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.implicit_credential_authorization_url.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.implicit_credential_authorization_url.redirect_uri = + Some(redirect_uri.as_ref().to_owned()); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.implicit_credential_authorization_url.authority = + Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.implicit_credential_authorization_url.authority = authority.into(); + self + } + + /// Default is code. Must include code for the authorization code flow. + /// Can also include id_token or token if using the hybrid flow. + pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( + &mut self, + response_type: I, + ) -> &mut Self { + self.implicit_credential_authorization_url.response_type = + response_type.into_iter().collect(); + self + } + + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - **query**: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter is not supported when requesting an + /// ID token by using the implicit flow. + /// - **fragment**: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - **form_post**: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. + pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { + self.implicit_credential_authorization_url.response_mode = response_mode; + self + } + + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { + self.implicit_credential_authorization_url.nonce = nonce.as_ref().to_owned(); + self + } + + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { + self.implicit_credential_authorization_url.state = Some(state.as_ref().to_owned()); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.implicit_credential_authorization_url.scope = + scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + /// Indicates the type of user interaction that is required. Valid values are login, none, + /// consent, and select_account. + /// + /// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on. + /// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt. + /// If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error. + /// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to + /// grant permissions to the app. + /// - **prompt=select_account** interrupts single sign-on providing account selection experience + /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. + pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { + self.implicit_credential_authorization_url.prompt = Some(prompt); + self + } + + pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { + self.implicit_credential_authorization_url.domain_hint = + Some(domain_hint.as_ref().to_owned()); + self + } + + pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { + self.implicit_credential_authorization_url.login_hint = + Some(login_hint.as_ref().to_owned()); + self + } + + pub fn build(&self) -> ImplicitCredentialAuthorizationUrl { + self.implicit_credential_authorization_url.clone() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn serialize_uri() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(vec![ResponseType::Token]) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_response_mode(ResponseMode::Fragment) + .with_state("12345") + .with_nonce("678910") + .with_prompt(Prompt::None) + .with_login_hint("myuser@mycompany.com") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + } + + #[test] + fn set_open_id_fragment() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(vec![ResponseType::IdToken]) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_mode=fragment")) + } + + #[test] + fn response_type_join() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_type=id_token+token")) + } + + #[test] + fn response_type_join_string() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(ResponseType::FromString(vec![ + "id_token".to_owned(), + "token".to_owned(), + ])) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_type=id_token+token")) + } + + #[test] + fn response_type_into_iter() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(ResponseType::IdToken) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_type=id_token")) + } + + #[test] + fn response_type_into_iter2() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_type=id_token+token")) + } +} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index c6dce850..45300ecf 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -5,9 +5,12 @@ mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; +mod implicit_credential_authorization_url; mod prompt; mod proof_key_for_code_exchange; +mod public_client_application; mod response_mode; +mod response_type; mod token_credential; mod token_request; @@ -21,11 +24,18 @@ pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; pub use confidential_client_application::*; +pub use implicit_credential_authorization_url::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; +pub use public_client_application::*; pub use response_mode::*; +pub use response_type::*; pub use token_credential::*; pub use token_request::*; #[cfg(feature = "openssl")] pub use client_assertion::*; + +// Powershell +// [System.Diagnostics.Tracing.EventSource]::new("graph-rs-sdk").Guid +pub static EVENT_TRACING_GUID: &str = "58c1e34e-8df1-5dfb-4a3c-6066550ab7f7"; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs new file mode 100644 index 00000000..c5101384 --- /dev/null +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -0,0 +1 @@ +pub struct PublicClientApplication {} diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs new file mode 100644 index 00000000..84aeb2f4 --- /dev/null +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -0,0 +1,34 @@ +#[derive(Clone, Debug, Default)] +pub enum ResponseType { + #[default] + Code, + Token, + IdToken, + FromString(Vec<String>), +} + +impl ToString for ResponseType { + fn to_string(&self) -> String { + match self { + ResponseType::Code => "code".to_owned(), + ResponseType::Token => "token".to_owned(), + ResponseType::IdToken => "id_token".to_owned(), + ResponseType::FromString(response_type_vec) => { + let response_types: Vec<String> = response_type_vec + .iter() + .map(|s| s.trim().to_owned()) + .collect(); + response_types.join(" ") + } + } + } +} + +impl IntoIterator for ResponseType { + type Item = ResponseType; + type IntoIter = std::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} From 0a0ed62bb1d0afb85532b06d1233bec635751640 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 23 Apr 2023 21:50:14 -0400 Subject: [PATCH 009/118] Implement AuthorizationSerializer for ClientCertificateCredential --- ...thorization_code_certificate_credential.rs | 29 +++- .../authorization_code_credential.rs | 33 ++++- .../client_certificate_credential.rs | 124 ++++++++++++++++-- .../implicit_credential_authorization_url.rs | 4 +- 4 files changed, 176 insertions(+), 14 deletions(-) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 3ddf1ef3..2ea5ddc0 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -13,6 +13,12 @@ use url::Url; #[cfg(feature = "openssl")] use crate::oauth::ClientAssertion; +/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application +/// to obtain authorized access to protected resources like web APIs. The auth code flow requires +/// a user-agent that supports redirection from the authorization server (the Microsoft +/// identity platform) back to your application. For example, a web browser, desktop, or mobile +/// application operated by a user to sign in to your app and access their data. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow #[derive(Clone)] pub struct AuthorizationCodeCertificateCredential { /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. @@ -20,12 +26,31 @@ pub struct AuthorizationCodeCertificateCredential { /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, - /// The client (application) ID of the service principal + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app pub(crate) client_id: String, + /// Optional + /// The redirect_uri of your app, where authentication responses can be sent and received + /// by your app. It must exactly match one of the redirect_uris you registered in the portal, + /// except it must be URL-encoded. pub(crate) redirect_uri: String, + /// The same code_verifier that was used to obtain the authorization_code. + /// Required if PKCE was used in the authorization code grant request. For more information, + /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, + /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. pub(crate) client_assertion_type: String, + /// An assertion (a JSON web token) that you need to create and sign with the certificate + /// you registered as credentials for your application. Read about certificate credentials + /// to learn how to register your certificate and the format of the assertion. pub(crate) client_assertion: String, + /// Required + /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the + /// scope openid, which translates to the "Sign you in" permission in the consent UI. + /// Optionally you may also want to include the email and profile scopes for gaining access + /// to additional user data. You may also include other scopes in this request for requesting + /// consent to various resources, if an access token is requested. pub(crate) scope: Vec<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, @@ -158,7 +183,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg( OAuthCredential::AuthorizationCode.alias(), - None, + Some("refresh_token is set but is empty"), ); } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 2d0f3e0c..9abe07a5 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -10,20 +10,49 @@ use reqwest::Response; use std::collections::HashMap; use url::Url; +/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application +/// to obtain authorized access to protected resources like web APIs. The auth code flow requires +/// a user-agent that supports redirection from the authorization server (the Microsoft +/// identity platform) back to your application. For example, a web browser, desktop, or mobile +/// application operated by a user to sign in to your app and access their data. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow #[derive(Clone)] pub struct AuthorizationCodeCredential { - /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. + /// Required unless requesting a refresh token + /// The authorization code obtained from a call to authorize. + /// The code should be obtained with all required scopes. pub(crate) authorization_code: Option<String>, + /// Required when requesting a new access token using a refresh token /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, - /// The client (application) ID of the service principal + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app pub(crate) client_id: String, + /// Required + /// The application secret that you created in the app registration portal for your app. + /// Don't use the application secret in a native app or single page app because a + /// client_secret can't be reliably stored on devices or web pages. It's required for web + /// apps and web APIs, which can store the client_secret securely on the server side. Like + /// all parameters here, the client secret must be URL-encoded before being sent. This step + /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax + /// specification. The Basic auth pattern of instead providing credentials in the Authorization + /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, + /// The same redirect_uri value that was used to acquire the authorization_code. pub(crate) redirect_uri: String, + /// A space-separated list of scopes. The scopes must all be from a single resource, + /// along with OIDC scopes (profile, openid, email). For more information, see Permissions + /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension + /// to the authorization code flow, intended to allow apps to declare the resource they want + /// the token for during token redemption. pub(crate) scopes: Vec<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, + /// The same code_verifier that was used to obtain the authorization_code. + /// Required if PKCE was used in the authorization code grant request. For more information, + /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 54272b92..3d20ba3c 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,9 +1,14 @@ -use crate::auth::OAuth; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; -use graph_error::{AuthorizationResult, GraphResult}; +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::{ + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, +}; +use async_trait::async_trait; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use reqwest::Response; use std::collections::HashMap; use url::Url; +use crate::identity::form_credential::FormCredential; #[cfg(feature = "openssl")] use crate::identity::ClientAssertion; @@ -12,7 +17,6 @@ use crate::identity::ClientAssertion; pub struct ClientCertificateCredential { /// The client (application) ID of the service principal pub(crate) client_id: String, - pub(crate) certificate: String, /// The value passed for the scope parameter in this request should be the resource /// identifier (application ID URI) of the resource you want, affixed with the .default /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. @@ -21,6 +25,8 @@ pub struct ClientCertificateCredential { pub(crate) authority: Authority, pub(crate) client_assertion_type: String, pub(crate) client_assertion: String, + pub(crate) refresh_token: Option<String>, + pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, } @@ -30,13 +36,96 @@ impl ClientCertificateCredential { } } +#[async_trait] +impl TokenRequest for ClientCertificateCredential { + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::blocking::Client::new(); + Ok(http_client.post(uri).form(&form).send()?) + } + + async fn get_token_async(&mut self) -> anyhow::Result<Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::Client::new(); + Ok(http_client.post(uri).form(&form).send().await?) + } +} + impl AuthorizationSerializer for ClientCertificateCredential { - fn uri(&mut self, _azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { - unimplemented!() + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + let uri = self + .serializer + .get_or_else(OAuthCredential::AccessTokenUrl)?; + Url::parse(uri.as_str()).map_err(GraphFailure::from) } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { - unimplemented!() + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_msg( + OAuthCredential::ClientId.alias(), + None, + ); + } + + if self.client_assertion.trim().is_empty() { + return AuthorizationFailure::required_value_msg( + OAuthCredential::ClientAssertion.alias(), + None, + ); + } + + if self.client_assertion_type.trim().is_empty() { + self.client_assertion_type = + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_owned(); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_assertion(self.client_assertion.as_str()) + .client_assertion_type(self.client_assertion_type.as_str()); + + if self.scopes.is_empty() { + self.serializer + .add_scope("https://graph.microsoft.com/.default"); + } + + return if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AuthorizationFailure::required_value_msg( + OAuthCredential::RefreshToken.alias(), + Some("refresh_token is set but is empty"), + ); + } + + self.serializer + .refresh_token(refresh_token.as_ref()) + .grant_type("refresh_token"); + + self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::RefreshToken), + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::ClientAssertion), + FormCredential::Required(OAuthCredential::ClientAssertionType), + ]) + } else { + self.serializer.grant_type("client_credentials"); + self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::ClientAssertion), + FormCredential::Required(OAuthCredential::ClientAssertionType), + ]) + }; } } @@ -49,12 +138,13 @@ impl ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { client_id: String::new(), - certificate: String::new(), scopes: vec![], authority: Default::default(), client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" .to_owned(), client_assertion: String::new(), + refresh_token: None, + token_credential_options: TokenCredentialOptions::default(), serializer: OAuth::new(), }, } @@ -88,6 +178,11 @@ impl ClientCertificateCredentialBuilder { self } + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); @@ -105,7 +200,20 @@ impl ClientCertificateCredentialBuilder { self } + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } + pub fn build(&self) -> ClientCertificateCredential { self.credential.clone() } } + +impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder { + fn from(credential: ClientCertificateCredential) -> Self { + ClientCertificateCredentialBuilder { credential } + } +} diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index 045b43f8..9b9ad691 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -120,7 +120,7 @@ impl ImplicitCredentialAuthorizationUrl { let mut serializer = OAuth::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg("client_id", None); + return AuthorizationFailure::required_value("client_id"); } if self.nonce.trim().is_empty() { @@ -155,7 +155,7 @@ impl ImplicitCredentialAuthorizationUrl { if response_type.contains("id_token") { serializer.add_scope("openid"); } else { - return AuthorizationFailure::required_value_msg("scope", None); + return AuthorizationFailure::required_value("scope"); } } From 94964334d4614fa7ebe320d97b3e014f6fa97071 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 24 Apr 2023 07:40:06 -0400 Subject: [PATCH 010/118] Implement AuthorizationSerializer as a required trait of TokenRequest --- graph-error/src/authorization_failure.rs | 18 +++++- graph-oauth/src/auth.rs | 6 +- ...rialize.rs => authorization_serializer.rs} | 4 +- .../auth_code_authorization_url.rs | 8 +-- ...thorization_code_certificate_credential.rs | 41 +++++-------- .../authorization_code_credential.rs | 60 ++++++++----------- .../client_certificate_credential.rs | 41 ++++--------- .../client_credentials_authorization_url.rs | 16 ++--- .../credentials/client_secret_credential.rs | 35 ++++++++--- .../confidential_client_application.rs | 21 ++++++- .../implicit_credential_authorization_url.rs | 11 ++-- .../src/identity/credentials/prompt.rs | 7 +++ .../src/identity/credentials/token_request.rs | 21 ++++++- graph-oauth/src/identity/mod.rs | 4 +- 14 files changed, 160 insertions(+), 133 deletions(-) rename graph-oauth/src/identity/{serialize.rs => authorization_serializer.rs} (77%) diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 3e7c66da..14fef70e 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -13,14 +13,28 @@ pub enum AuthorizationFailure { } impl AuthorizationFailure { - pub fn required_value<T: AsRef<str>, U>(name: T) -> AuthorizationResult<U> { + pub fn required_value<T: AsRef<str>>(name: T) -> AuthorizationFailure { + AuthorizationFailure::RequiredValue { + name: name.as_ref().to_owned(), + message: None, + } + } + + pub fn required_value_result<T: AsRef<str>, U>(name: T) -> AuthorizationResult<U> { Err(AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), message: None, }) } - pub fn required_value_msg<T>( + pub fn required_value_msg<T: AsRef<str>>(name: T, message: Option<T>) -> AuthorizationFailure { + AuthorizationFailure::RequiredValue { + name: name.as_ref().to_owned(), + message: message.map(|s| s.as_ref().to_owned()), + } + } + + pub fn required_value_msg_result<T>( name: &str, message: Option<&str>, ) -> Result<T, AuthorizationFailure> { diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index e6c03a22..ff4017f3 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1046,7 +1046,7 @@ impl OAuth { FormCredential::Required(oac) => { if oac.alias().eq("scope") { if self.scopes.is_empty() { - return AuthorizationFailure::required_value_msg::<()>( + return AuthorizationFailure::required_value_msg_result::<()>( oac.alias(), None, ); @@ -1056,7 +1056,7 @@ impl OAuth { } else if let Some(val) = self.get(*oac) { encoder.append_pair(oac.alias(), val.as_str()); } else { - return AuthorizationFailure::required_value_msg::<()>( + return AuthorizationFailure::required_value_msg_result::<()>( oac.alias(), None, ); @@ -1108,7 +1108,7 @@ impl OAuth { message: None, })?; if val.trim().is_empty() { - return AuthorizationFailure::required_value(oac); + return AuthorizationFailure::required_value_result(oac); } else { map.insert(oac.to_string(), val); } diff --git a/graph-oauth/src/identity/serialize.rs b/graph-oauth/src/identity/authorization_serializer.rs similarity index 77% rename from graph-oauth/src/identity/serialize.rs rename to graph-oauth/src/identity/authorization_serializer.rs index ab964910..553b04d5 100644 --- a/graph-oauth/src/identity/serialize.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -1,9 +1,9 @@ use crate::identity::AzureAuthorityHost; -use graph_error::{AuthorizationResult, GraphResult}; +use graph_error::AuthorizationResult; use std::collections::HashMap; use url::Url; pub trait AuthorizationSerializer { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url>; + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; fn form(&mut self) -> AuthorizationResult<HashMap<String, String>>; } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index bf0171a2..82423654 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -76,15 +76,15 @@ impl AuthCodeAuthorizationUrl { let mut serializer = OAuth::new(); if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_msg("redirect_uri", None); + return AuthorizationFailure::required_value_msg_result("redirect_uri", None); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg("client_id", None); + return AuthorizationFailure::required_value_msg_result("client_id", None); } if self.scope.is_empty() { - return AuthorizationFailure::required_value_msg("scope", None); + return AuthorizationFailure::required_value_msg_result("scope", None); } serializer @@ -168,7 +168,7 @@ impl AuthCodeAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg("authorization_url", None) + AuthorizationFailure::required_value_msg_result("authorization_url", None) } } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 2ea5ddc0..4d79a1b0 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -5,8 +5,7 @@ use crate::identity::{ TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; -use reqwest::Response; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -89,37 +88,25 @@ impl AuthorizationCodeCertificateCredential { #[async_trait] impl TokenRequest for AuthorizationCodeCertificateCredential { - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::blocking::Client::new(); - Ok(http_client.post(uri).form(&form).send()?) - } - - async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::Client::new(); - Ok(http_client.post(uri).form(&form).send().await?) + fn azure_authority_host(&self) -> &AzureAuthorityHost { + &self.token_credential_options.azure_authority_host } } impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self - .serializer - .get_or_else(OAuthCredential::AccessTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -130,14 +117,14 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::ClientId.alias(), None, ); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::ClientAssertion.alias(), None, ); @@ -161,7 +148,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::RefreshToken.alias(), None, ); @@ -181,7 +168,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::AuthorizationCode.alias(), Some("refresh_token is set but is empty"), ); @@ -203,7 +190,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { ]); } - AuthorizationFailure::required_value_msg( + AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 9abe07a5..a594998d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -5,8 +5,7 @@ use crate::identity::{ ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; -use reqwest::Response; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -90,46 +89,49 @@ impl AuthorizationCodeCredential { } impl AuthorizationSerializer for AuthorizationCodeCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self - .serializer - .get_or_else(OAuthCredential::AccessTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( + "access_token_url", + Some("Internal Error"), + ), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { let uri = self .serializer - .get_or_else(OAuthCredential::RefreshTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + .get(OAuthCredential::RefreshTokenUrl) + .ok_or(AuthorizationFailure::required_value_msg( + "refresh_token_url", + Some("Internal Error"), + ))?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), OAuthCredential::RefreshToken.alias() ), - Some("Authorization code and refresh token cannot be set at the same time - choose one or the other"), + Some("Authorization code and refresh token should not be set at the same time - Internal Error"), ); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg( - OAuthCredential::ClientId.alias(), - None, - ); + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_result( OAuthCredential::ClientSecret.alias(), - None, ); } @@ -140,7 +142,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); @@ -159,14 +161,14 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::RedirectUri); + return AuthorizationFailure::required_value_result(OAuthCredential::RedirectUri); } self.serializer @@ -189,7 +191,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { ]); } - AuthorizationFailure::required_value_msg( + AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", OAuthCredential::AuthorizationCode.alias(), @@ -202,20 +204,8 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { #[async_trait] impl TokenRequest for AuthorizationCodeCredential { - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::blocking::Client::new(); - Ok(http_client.post(uri).form(&form).send()?) - } - - async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::Client::new(); - Ok(http_client.post(uri).form(&form).send().await?) + fn azure_authority_host(&self) -> &AzureAuthorityHost { + &self.token_credential_options.azure_authority_host } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 3d20ba3c..4c3e2bc2 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,14 +1,13 @@ use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::form_credential::FormCredential; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; -use reqwest::Response; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; -use crate::identity::form_credential::FormCredential; #[cfg(feature = "openssl")] use crate::identity::ClientAssertion; @@ -38,46 +37,30 @@ impl ClientCertificateCredential { #[async_trait] impl TokenRequest for ClientCertificateCredential { - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::blocking::Client::new(); - Ok(http_client.post(uri).form(&form).send()?) - } - - async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); - let uri = self.uri(&azure_authority_host)?; - let form = self.form()?; - let http_client = reqwest::Client::new(); - Ok(http_client.post(uri).form(&form).send().await?) + fn azure_authority_host(&self) -> &AzureAuthorityHost { + &self.token_credential_options.azure_authority_host } } impl AuthorizationSerializer for ClientCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self - .serializer - .get_or_else(OAuthCredential::AccessTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg( - OAuthCredential::ClientId.alias(), - None, - ); + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_result( OAuthCredential::ClientAssertion.alias(), - None, ); } @@ -98,7 +81,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { return if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_msg_result( OAuthCredential::RefreshToken.alias(), Some("refresh_token is set but is empty"), ); diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 0b0dfe66..a97e0bff 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -37,16 +37,12 @@ impl ClientCredentialsAuthorizationUrl { ) -> AuthorizationResult<Url> { let mut serializer = OAuth::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg( - OAuthCredential::ClientId.alias(), - None, - ); + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_msg( + return AuthorizationFailure::required_value_result( OAuthCredential::RedirectUri.alias(), - None, ); } @@ -72,16 +68,14 @@ impl ClientCredentialsAuthorizationUrl { let mut url = Url::parse( serializer - .get_or_else(OAuthCredential::AuthorizationUrl) - .or(AuthorizationFailure::required_value_msg( + .get(OAuthCredential::AuthorizationUrl) + .ok_or(AuthorizationFailure::required_value( OAuthCredential::AuthorizationUrl.alias(), - None, ))? .as_str(), ) - .or(AuthorizationFailure::required_value_msg( + .or(AuthorizationFailure::required_value_result( OAuthCredential::AuthorizationUrl.alias(), - None, ))?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index fa537f31..64b2ef82 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,7 +1,8 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::identity::form_credential::FormCredential; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, TokenRequest}; +use crate::oauth::TokenCredentialOptions; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -26,6 +27,7 @@ pub struct ClientSecretCredential { /// Default is https://graph.microsoft.com/.default. pub(crate) scopes: Vec<String>, pub(crate) authority: Authority, + pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, } @@ -36,6 +38,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scopes: vec!["https://graph.microsoft.com/.default".to_owned()], authority: Default::default(), + token_credential_options: Default::default(), serializer: OAuth::new(), } } @@ -45,24 +48,30 @@ impl ClientSecretCredential { } } +impl TokenRequest for ClientSecretCredential { + fn azure_authority_host(&self) -> &AzureAuthorityHost { + &self.token_credential_options.azure_authority_host + } +} + impl AuthorizationSerializer for ClientSecretCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> GraphResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self - .serializer - .get_or_else(OAuthCredential::AccessTokenUrl)?; - Url::parse(uri.as_str()).map_err(GraphFailure::from) + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientId); + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value(OAuthCredential::ClientSecret); + return AuthorizationFailure::required_value_result(OAuthCredential::ClientSecret); } self.serializer @@ -98,6 +107,7 @@ impl ClientSecretCredentialBuilder { client_secret: String::new(), scopes: vec![], authority: Default::default(), + token_credential_options: Default::default(), serializer: OAuth::new(), }, } @@ -129,6 +139,13 @@ impl ClientSecretCredentialBuilder { self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); self } + + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } } impl Default for ClientSecretCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 34be3ec2..46de1b28 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,10 +1,13 @@ use crate::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - ClientCertificateCredential, ClientSecretCredential, TokenCredentialOptions, TokenRequest, + AzureAuthorityHost, ClientCertificateCredential, ClientSecretCredential, + TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::GraphResult; +use graph_error::{AuthorizationResult, GraphResult}; use reqwest::Response; +use std::collections::HashMap; +use url::Url; pub struct ConfidentialClientApplication { http_client: reqwest::Client, @@ -26,8 +29,22 @@ impl ConfidentialClientApplication { } } +impl AuthorizationSerializer for ConfidentialClientApplication { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.credential.uri(azure_authority_host) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form() + } +} + #[async_trait] impl TokenRequest for ConfidentialClientApplication { + fn azure_authority_host(&self) -> &AzureAuthorityHost { + &self.token_credential_options.azure_authority_host + } + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { let uri = self .credential diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index 9b9ad691..61a5d316 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -120,11 +120,11 @@ impl ImplicitCredentialAuthorizationUrl { let mut serializer = OAuth::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value("client_id"); + return AuthorizationFailure::required_value_result("client_id"); } if self.nonce.trim().is_empty() { - return AuthorizationFailure::required_value("nonce"); + return AuthorizationFailure::required_value_result("nonce"); } serializer @@ -155,7 +155,7 @@ impl ImplicitCredentialAuthorizationUrl { if response_type.contains("id_token") { serializer.add_scope("openid"); } else { - return AuthorizationFailure::required_value("scope"); + return AuthorizationFailure::required_value_result("scope"); } } @@ -196,7 +196,10 @@ impl ImplicitCredentialAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg("authorization_url", Some("Internal Error")) + AuthorizationFailure::required_value_msg_result( + "authorization_url", + Some("Internal Error"), + ) } } } diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index e7680ccc..c389c79c 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -12,8 +12,15 @@ pub enum Prompt { #[default] None, + /// The user will be prompted for credentials by the service. It is achieved + /// by sending <prompt=login to the Azure AD service. Login, + /// The user will be prompted to consent even if consent was granted before. It is achieved + /// by sending prompt=consent to Azure AD. Consent, + /// AcquireToken will send prompt=select_account to Azure AD's authorize endpoint + /// which would present to the user a list of accounts from which one can be selected for + /// authentication. SelectAccount, } diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index c88d62ec..9b5077d1 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,7 +1,22 @@ +use crate::oauth::{AuthorizationSerializer, AzureAuthorityHost}; use async_trait::async_trait; #[async_trait] -pub trait TokenRequest { - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response>; - async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response>; +pub trait TokenRequest: AuthorizationSerializer { + fn azure_authority_host(&self) -> &AzureAuthorityHost; + + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.azure_authority_host().clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::blocking::Client::new(); + Ok(http_client.post(uri).form(&form).send()?) + } + async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { + let azure_authority_host = self.azure_authority_host().clone(); + let uri = self.uri(&azure_authority_host)?; + let form = self.form()?; + let http_client = reqwest::Client::new(); + Ok(http_client.post(uri).form(&form).send().await?) + } } diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index a932d82b..77eee150 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,11 +1,11 @@ mod authority; +mod authorization_serializer; mod credentials; pub(crate) mod form_credential; -mod serialize; pub use authority::*; +pub use authorization_serializer::*; pub use credentials::*; -pub use serialize::*; #[cfg(feature = "openssl")] pub use openssl::{ From 951bd0740f6ac61ee1048af2262bf4d456ee881c Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 30 Apr 2023 01:31:51 -0400 Subject: [PATCH 011/118] Add device code credential, webview setup for interactive auth --- examples/oauth/code_flow.rs | 152 ----------- examples/oauth/main.rs | 1 - graph-oauth/Cargo.toml | 1 + graph-oauth/src/access_token.rs | 61 ++++- graph-oauth/src/auth.rs | 12 +- graph-oauth/src/device_code.rs | 15 ++ graph-oauth/src/id_token.rs | 14 +- graph-oauth/src/identity/authority.rs | 6 + .../auth_code_authorization_url.rs | 12 +- ...thorization_code_certificate_credential.rs | 12 +- .../authorization_code_credential.rs | 32 ++- .../identity/credentials/client_assertion.rs | 7 +- .../client_certificate_credential.rs | 9 +- .../client_credentials_authorization_url.rs | 2 +- .../credentials/client_secret_credential.rs | 23 +- .../code_flow_authorization_url.rs | 127 +++++++++ .../credentials/code_flow_credential.rs | 226 ++++++++++++++++ .../confidential_client_application.rs | 24 +- .../credentials/device_code_credential.rs | 242 ++++++++++++++++++ graph-oauth/src/identity/credentials/mod.rs | 12 +- .../src/identity/credentials/prompt.rs | 4 + .../token_flow_authorization_url.rs | 114 +++++++++ .../src/identity/credentials/token_request.rs | 12 +- graph-oauth/src/lib.rs | 3 + graph-oauth/src/web/interactive_web_view.rs | 68 +++++ graph-oauth/src/web/mod.rs | 3 + 26 files changed, 977 insertions(+), 217 deletions(-) delete mode 100644 examples/oauth/code_flow.rs create mode 100644 graph-oauth/src/device_code.rs create mode 100644 graph-oauth/src/identity/credentials/code_flow_authorization_url.rs create mode 100644 graph-oauth/src/identity/credentials/code_flow_credential.rs create mode 100644 graph-oauth/src/identity/credentials/device_code_credential.rs create mode 100644 graph-oauth/src/identity/credentials/token_flow_authorization_url.rs create mode 100644 graph-oauth/src/web/interactive_web_view.rs create mode 100644 graph-oauth/src/web/mod.rs diff --git a/examples/oauth/code_flow.rs b/examples/oauth/code_flow.rs deleted file mode 100644 index 4c124c77..00000000 --- a/examples/oauth/code_flow.rs +++ /dev/null @@ -1,152 +0,0 @@ -use graph_oauth::oauth::AccessToken; -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -/// -/// # Setup: -/// This example shows using the OneDrive and SharePoint code flow: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online -/// Includes authorization with a state parameter in the request query. The state parameter is optional. -/// -/// You will first need to head to the Microsoft Application Portal and create and -/// application. Once the application is created you will need to specify the -/// scopes you need and change them accordingly in the oauth_web_client() method -/// when adding scopes using OAuth::add_scope("scope"). -/// -/// For reference the Microsoft Graph Authorization V2 required parameters along with -/// the methods to use needed to be set are shown in the oauth_web_client() method. -/// -/// Once an application is registered in Azure you will be given an application id which is the client id in an OAuth2 request. -/// For this example, a client secret will need to be generated. The client secret is the same as the password -/// under Application Secrets int the registration portal. If you do not have a client secret then click on -/// 'Generate New Password'. Next click on 'Add Platform' and create a new web platform. -/// Add a redirect url to the platform. In the example below, the redirect url is http://localhost:8000/redirect -/// but anything can be used. -/// -/// # Sign In Flow: -/// -/// After signing in, you will be redirected, and the access code that is given in the redirect -/// will be used to automatically call the access token endpoint and receive an access token -/// and/or refresh token. -/// -/// Disclaimer/Important Info: -/// -/// This example is meant for testing and is not meant to be production ready or complete. -use graph_rs_sdk::oauth::OAuth; -use warp::Filter; - -// The client_id and client_secret must be changed before running this example. -static CLIENT_ID: &str = "<YOUR_CLIENT_ID>"; -static CLIENT_SECRET: &str = "<YOUR_CLIENT_SECRET>"; - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct AccessCode { - code: String, - state: String, -} - -// Create OAuth client and set credentials. -fn oauth_web_client() -> OAuth { - let mut oauth = OAuth::new(); - oauth - .client_id(CLIENT_ID) - .client_secret(CLIENT_SECRET) - .add_scope("Files.Read") - .add_scope("Files.ReadWrite") - .add_scope("Files.Read.All") - .add_scope("Files.ReadWrite.All") - .add_scope("wl.offline_access") - .redirect_uri("http://localhost:8000/redirect") - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .access_token_url("https://login.live.com/oauth20_token.srf") - .refresh_token_url("https://login.live.com/oauth20_token.srf") - .response_mode("query") - .state("13534298") // Optional - .logout_url("https://login.live.com/oauth20_logout.srf?") // Optional - // The redirect_url given above will be used for the logout redirect if none is provided. - .post_logout_redirect_uri("http://localhost:8000/redirect"); // Optional - oauth -} - -pub async fn set_and_req_access_code(access_code: AccessCode) { - let mut oauth = oauth_web_client(); - oauth.response_type("token"); - oauth.state(access_code.state.as_str()); - oauth.authorization_code(access_code.code.as_str()); - - // Request the access token. - let mut client = oauth.build_async().code_flow(); - - let response = client.access_token().send().await.unwrap(); - println!("{response:#?}"); - - if response.status().is_success() { - let access_token: AccessToken = response.json().await.unwrap(); - - println!("{access_token:#?}"); - oauth.access_token(access_token); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - } - - // If all went well here we can print out the OAuth config with the Access Token. - println!("{:#?}", &oauth); -} - -async fn handle_redirect( - code_option: Option<AccessCode>, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - match code_option { - Some(access_code) => { - // Print out the code for debugging purposes. - println!("{access_code:#?}"); - - // Assert that the state is the same as the one given in the original request. - assert_eq!("13534298", access_code.state.as_str()); - - // Set the access code and request an access token. - // Callers should handle the Result from requesting an access token - // in case of an error here. - set_and_req_access_code(access_code).await; - - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } -} - -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<AccessCode>() - .map(Some) - .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); - - let mut oauth = oauth_web_client(); - let mut request = oauth.build_async().code_flow(); - request.browser_authorization().open().unwrap(); - - warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; -} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 90c19486..2330925e 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -20,7 +20,6 @@ mod auth_code_grant_pkce; mod auth_code_grant_refresh_token; mod client_credentials; mod client_credentials_admin_consent; -mod code_flow; mod device_code; mod implicit_grant; mod is_access_token_expired; diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 4d1edd03..bd33e68a 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -27,6 +27,7 @@ serde_json = "1" strum = { version = "0.24.1", features = ["derive"] } url = "2" webbrowser = "0.8.7" +wry = "0.28.3" uuid = { version = "1.3.1", features = ["v4"] } graph-error = { path = "../graph-error" } diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 3de4400b..ac3de6ea 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -4,7 +4,10 @@ use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc}; use chrono_humanize::HumanTime; use graph_error::GraphFailure; use serde_aux::prelude::*; +use serde_json::Value; +use std::collections::HashMap; use std::fmt; +use std::str::FromStr; /// OAuth 2.0 Access Token /// @@ -59,6 +62,11 @@ pub struct AccessToken { timestamp: Option<DateTime<Utc>>, #[serde(skip)] jwt: Option<JsonWebToken>, + /// Any extra returned fields for AccessToken. + #[serde(flatten)] + additional_fields: HashMap<String, Value>, + #[serde(skip)] + log_pii: bool, } impl AccessToken { @@ -74,6 +82,8 @@ impl AccessToken { state: None, timestamp: Some(Utc::now() + Duration::seconds(expires_in)), jwt: None, + additional_fields: Default::default(), + log_pii: false, }; token.parse_jwt(); token @@ -191,6 +201,10 @@ impl AccessToken { self.id_token = Some(id_token.get_id_token()); } + pub fn parse_id_token(&mut self) -> Option<Result<IdToken, std::io::Error>> { + self.id_token.clone().map(|s| IdToken::from_str(s.as_str())) + } + /// Set the state. /// /// # Example @@ -206,6 +220,15 @@ impl AccessToken { self } + /// Enable or disable logging of personally identifiable information such + /// as logging the id_token. This is disabled by default. When log_pii is enabled + /// passing [AccessToken] to logging or print functions will log both the bearer + /// access token value, the refresh token value if any, and the id token value. + /// By default these do not get logged. + pub fn enable_pii_logging(&mut self, log_pii: bool) { + self.log_pii = log_pii; + } + /// Reset the access token timestmap. /// /// # Example @@ -436,6 +459,8 @@ impl Default for AccessToken { state: None, timestamp: Some(Utc::now() + Duration::seconds(0)), jwt: None, + additional_fields: Default::default(), + log_pii: false, } } } @@ -483,16 +508,32 @@ impl TryFrom<reqwest::blocking::Response> for AccessToken { impl fmt::Debug for AccessToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("AccessToken") - .field("bearer_token", &"[REDACTED]") - .field("token_type", &self.token_type) - .field("expires_in", &self.expires_in) - .field("scope", &self.scope) - .field("user_id", &self.user_id) - .field("id_token", &"[REDACTED]") - .field("state", &self.state) - .field("timestamp", &self.timestamp) - .finish() + if self.log_pii { + f.debug_struct("AccessToken") + .field("bearer_token", &self.access_token) + .field("refresh_token", &self.refresh_token) + .field("token_type", &self.token_type) + .field("expires_in", &self.expires_in) + .field("scope", &self.scope) + .field("user_id", &self.user_id) + .field("id_token", &self.id_token) + .field("state", &self.state) + .field("timestamp", &self.timestamp) + .field("extra", &self.additional_fields) + .finish() + } else { + f.debug_struct("AccessToken") + .field("bearer_token", &"[REDACTED]") + .field("token_type", &self.token_type) + .field("expires_in", &self.expires_in) + .field("scope", &self.scope) + .field("user_id", &self.user_id) + .field("id_token", &"[REDACTED]") + .field("state", &self.state) + .field("timestamp", &self.timestamp) + .field("extra", &self.additional_fields) + .finish() + } } } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index ff4017f3..33f55419 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -385,6 +385,10 @@ impl OAuth { /// oauth.tenant_id("tenant_id"); /// ``` pub fn authority(&mut self, host: &AzureAuthorityHost, authority: &Authority) -> &mut OAuth { + if host.eq(&AzureAuthorityHost::OneDriveAndSharePoint) { + return self.legacy_authority(); + } + let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); let auth_url = format!( "{}/{}/oauth2/v2.0/authorize", @@ -410,6 +414,12 @@ impl OAuth { .refresh_token_url(&token_url) } + pub fn legacy_authority(&mut self) -> &mut OAuth { + self.authorization_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); + self.access_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); + self.refresh_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()) + } + /// Set the redirect url of a request /// /// # Example @@ -454,7 +464,7 @@ impl OAuth { /// # let mut oauth = OAuth::new(); /// oauth.response_type("token"); /// ``` - pub fn response_type(&mut self, value: &str) -> &mut OAuth { + pub fn response_type<T: ToString>(&mut self, value: T) -> &mut OAuth { self.insert(OAuthCredential::ResponseType, value) } diff --git a/graph-oauth/src/device_code.rs b/graph-oauth/src/device_code.rs new file mode 100644 index 00000000..4e8c0853 --- /dev/null +++ b/graph-oauth/src/device_code.rs @@ -0,0 +1,15 @@ +use serde_json::Value; +use std::collections::HashMap; +use std::time::Duration; + +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct DeviceCode { + pub device_code: String, + pub expires_in: u64, + pub interval: Duration, + pub message: String, + pub user_code: String, + pub verification_uri: String, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, +} diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index 8815993f..60ca611f 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -1,15 +1,21 @@ +use serde_json::Value; use std::borrow::Cow; +use std::collections::HashMap; use std::convert::TryFrom; use std::io::ErrorKind; use std::str::FromStr; use url::form_urlencoded; -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct IdToken { code: Option<String>, id_token: String, state: Option<String>, session_state: Option<String>, + #[serde(flatten)] + additional_fields: HashMap<String, Value>, + #[serde(skip)] + log_pii: bool, } impl IdToken { @@ -19,6 +25,8 @@ impl IdToken { id_token: id_token.into(), state: Some(state.into()), session_state: Some(session_state.into()), + additional_fields: Default::default(), + log_pii: false, } } @@ -38,6 +46,10 @@ impl IdToken { self.session_state = Some(session_state.into()); } + pub fn log_pii(&mut self, log_pii: bool) { + self.log_pii = log_pii; + } + pub fn get_id_token(&self) -> String { self.id_token.clone() } diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 8e6c2f5f..226fedbb 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -16,6 +16,8 @@ pub enum AzureAuthorityHost { AzureGermany, /// US Government cloud. Maps to https://login.microsoftonline.us AzureUsGovernment, + + OneDriveAndSharePoint, } impl AsRef<str> for AzureAuthorityHost { @@ -26,6 +28,9 @@ impl AsRef<str> for AzureAuthorityHost { AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn", AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de", AzureAuthorityHost::AzureUsGovernment => "https://login.microsoftonline.us", + AzureAuthorityHost::OneDriveAndSharePoint => { + "https://login.live.com/oauth20_desktop.srf" + } } } } @@ -52,6 +57,7 @@ impl AzureAuthorityHost { AzureAuthorityHost::AzureUsGovernment => { "https://management.usgovcloudapi.net/.default" } + AzureAuthorityHost::OneDriveAndSharePoint => "", } } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 82423654..9299feac 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -20,7 +20,7 @@ use url::Url; /// by a user to sign in to your app and access their data. /// /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AuthCodeAuthorizationUrl { /// The client (application) ID of the service principal pub(crate) client_id: String, @@ -168,7 +168,10 @@ impl AuthCodeAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg_result("authorization_url", None) + AuthorizationFailure::required_value_msg_result( + "authorization_url", + Some("Internal Error"), + ) } } } @@ -273,6 +276,11 @@ impl AuthCodeAuthorizationUrlBuilder { self } + /// Automatically adds profile, id_token, and offline_access to the scope parameter. + pub fn with_default_scope(&mut self) -> &mut Self { + self.with_scope(vec!["profile", "id_token", "offline_access"]) + } + /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 4d79a1b0..9c1f6f72 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,8 +1,8 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::identity::form_credential::FormCredential; use crate::identity::{ - AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, - TokenCredentialOptions, TokenRequest, + AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, + AzureAuthorityHost, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; @@ -84,12 +84,16 @@ impl AuthorizationCodeCertificateCredential { pub fn builder() -> AuthorizationCodeCertificateCredentialBuilder { AuthorizationCodeCertificateCredentialBuilder::new() } + + pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlBuilder { + AuthCodeAuthorizationUrlBuilder::new() + } } #[async_trait] impl TokenRequest for AuthorizationCodeCertificateCredential { - fn azure_authority_host(&self) -> &AzureAuthorityHost { - &self.token_credential_options.azure_authority_host + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index a594998d..5b42e990 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -4,6 +4,7 @@ use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, }; +use crate::oauth::AuthCodeAuthorizationUrlBuilder; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -46,7 +47,7 @@ pub struct AuthorizationCodeCredential { /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension /// to the authorization code flow, intended to allow apps to declare the resource they want /// the token for during token redemption. - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, /// The same code_verifier that was used to obtain the authorization_code. @@ -70,7 +71,7 @@ impl AuthorizationCodeCredential { client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), - scopes: vec![], + scope: vec![], authority: Default::default(), code_verifier: None, token_credential_options: TokenCredentialOptions::default(), @@ -86,6 +87,17 @@ impl AuthorizationCodeCredential { pub fn builder() -> AuthorizationCodeCredentialBuilder { AuthorizationCodeCredentialBuilder::new() } + + pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlBuilder { + AuthCodeAuthorizationUrlBuilder::new() + } +} + +#[async_trait] +impl TokenRequest for AuthorizationCodeCredential { + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } } impl AuthorizationSerializer for AuthorizationCodeCredential { @@ -138,7 +150,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { self.serializer .client_id(self.client_id.as_str()) .client_secret(self.client_secret.as_str()) - .extend_scopes(self.scopes.clone()); + .extend_scopes(self.scope.clone()); if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { @@ -162,7 +174,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthCredential::AuthorizationCode.alias(), Some("Either authorization code or refresh token is required"), ); } @@ -202,13 +214,6 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } } -#[async_trait] -impl TokenRequest for AuthorizationCodeCredential { - fn azure_authority_host(&self) -> &AzureAuthorityHost { - &self.token_credential_options.azure_authority_host - } -} - #[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { credential: AuthorizationCodeCredential, @@ -223,7 +228,7 @@ impl AuthorizationCodeCredentialBuilder { client_id: String::new(), client_secret: String::new(), redirect_uri: String::new(), - scopes: vec![], + scope: vec![], authority: Default::default(), code_verifier: None, token_credential_options: TokenCredentialOptions::default(), @@ -234,6 +239,7 @@ impl AuthorizationCodeCredentialBuilder { pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); + self.credential.refresh_token = None; self } @@ -283,7 +289,7 @@ impl AuthorizationCodeCredentialBuilder { } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + self.credential.scope = scopes.into_iter().map(|s| s.to_string()).collect(); self } diff --git a/graph-oauth/src/identity/credentials/client_assertion.rs b/graph-oauth/src/identity/credentials/client_assertion.rs index 5e35efbc..15308356 100644 --- a/graph-oauth/src/identity/credentials/client_assertion.rs +++ b/graph-oauth/src/identity/credentials/client_assertion.rs @@ -101,6 +101,11 @@ impl ClientAssertion { &self.uuid } + /// Set the UUID for the jti field of the claims/payload of the jwt. + pub fn set_uuid(&mut self, value: Uuid) { + self.uuid = value; + } + fn get_header(&self) -> Result<HashMap<String, String>, ErrorStack> { let mut header = HashMap::new(); header.insert("x5t".to_owned(), self.get_thumbprint_base64()?); @@ -138,7 +143,7 @@ impl ClientAssertion { claims.insert("aud".to_owned(), aud); claims.insert("exp".to_owned(), exp.as_secs().to_string()); claims.insert("nbf".to_owned(), nbf.as_secs().to_string()); - claims.insert("jti".to_owned(), Uuid::new_v4().to_string()); + claims.insert("jti".to_owned(), self.uuid.to_string()); claims.insert("sub".to_owned(), self.client_id.to_owned()); claims.insert("iss".to_owned(), self.client_id.to_owned()); diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 4c3e2bc2..785c55a9 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -10,6 +10,7 @@ use url::Url; #[cfg(feature = "openssl")] use crate::identity::ClientAssertion; +use crate::oauth::ClientCredentialsAuthorizationUrlBuilder; #[derive(Clone)] #[allow(dead_code)] @@ -33,12 +34,16 @@ impl ClientCertificateCredential { pub fn builder() -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder::new() } + + pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new() + } } #[async_trait] impl TokenRequest for ClientCertificateCredential { - fn azure_authority_host(&self) -> &AzureAuthorityHost { - &self.token_credential_options.azure_authority_host + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index a97e0bff..593b0058 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -87,7 +87,7 @@ pub struct ClientCredentialsAuthorizationUrlBuilder { } impl ClientCredentialsAuthorizationUrlBuilder { - fn new() -> Self { + pub fn new() -> Self { Self { credential: ClientCredentialsAuthorizationUrl { client_id: String::new(), diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 64b2ef82..be4862a9 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,6 +1,9 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::identity::form_credential::FormCredential; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, TokenRequest}; +use crate::identity::{ + Authority, AuthorizationSerializer, AzureAuthorityHost, + ClientCredentialsAuthorizationUrlBuilder, TokenRequest, +}; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -36,7 +39,7 @@ impl ClientSecretCredential { ClientSecretCredential { client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - scopes: vec!["https://graph.microsoft.com/.default".to_owned()], + scopes: vec![], authority: Default::default(), token_credential_options: Default::default(), serializer: OAuth::new(), @@ -46,11 +49,15 @@ impl ClientSecretCredential { pub fn builder() -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new() } + + pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new() + } } impl TokenRequest for ClientSecretCredential { - fn azure_authority_host(&self) -> &AzureAuthorityHost { - &self.token_credential_options.azure_authority_host + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options } } @@ -135,8 +142,8 @@ impl ClientSecretCredentialBuilder { } /// Defaults to "https://graph.microsoft.com/.default" - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.credential.scopes = scope.into_iter().map(|s| s.to_string()).collect(); self } @@ -146,6 +153,10 @@ impl ClientSecretCredentialBuilder { ) { self.credential.token_credential_options = token_credential_options; } + + pub fn build(&self) -> ClientSecretCredential { + self.credential.clone() + } } impl Default for ClientSecretCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs new file mode 100644 index 00000000..c36a1014 --- /dev/null +++ b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs @@ -0,0 +1,127 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::oauth::form_credential::FormCredential; +use crate::oauth::ResponseType; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use url::form_urlencoded::Serializer; +use url::Url; + +/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive +/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. +/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow +#[derive(Clone)] +pub struct CodeFlowAuthorizationUrl { + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required + /// The same redirect_uri value that was used to acquire the authorization_code. + pub(crate) redirect_uri: String, + /// Required + /// Must be code for code flow. + pub(crate) response_type: ResponseType, + /// Required + /// A space-separated list of scopes. The scopes must all be from a single resource, + /// along with OIDC scopes (profile, openid, email). For more information, see Permissions + /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension + /// to the authorization code flow, intended to allow apps to declare the resource they want + /// the token for during token redemption. + pub(crate) scope: Vec<String>, +} + +impl CodeFlowAuthorizationUrl { + pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + client_id: T, + redirect_uri: T, + scope: I, + ) -> CodeFlowAuthorizationUrl { + CodeFlowAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + response_type: ResponseType::Code, + scope: scope.into_iter().map(|s| s.to_string()).collect(), + } + } + + pub fn builder() -> CodeFlowAuthorizationUrlBuilder { + CodeFlowAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + let mut serializer = OAuth::new(); + if self.redirect_uri.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result("redirect_uri", None); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result("client_id", None); + } + + if self.scope.is_empty() { + return AuthorizationFailure::required_value_msg_result("scope", None); + } + + serializer + .client_id(self.client_id.as_str()) + .redirect_uri(self.redirect_uri.as_str()) + .extend_scopes(self.scope.clone()) + .legacy_authority() + .response_type(self.response_type.clone()); + + let mut encoder = Serializer::new(String::new()); + serializer.url_query_encode( + vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::ResponseType), + ], + &mut encoder, + )?; + + if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } else { + AuthorizationFailure::required_value_msg_result( + "authorization_url", + Some("Internal Error"), + ) + } + } +} + +#[derive(Clone)] +pub struct CodeFlowAuthorizationUrlBuilder { + token_flow_authorization_url: CodeFlowAuthorizationUrl, +} + +impl CodeFlowAuthorizationUrlBuilder { + fn new() -> CodeFlowAuthorizationUrlBuilder { + CodeFlowAuthorizationUrlBuilder { + token_flow_authorization_url: CodeFlowAuthorizationUrl { + client_id: String::new(), + redirect_uri: String::new(), + response_type: ResponseType::Code, + scope: vec![], + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.token_flow_authorization_url.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.token_flow_authorization_url.scope = + scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.token_flow_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } +} diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs new file mode 100644 index 00000000..6a07acc9 --- /dev/null +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -0,0 +1,226 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::form_credential::FormCredential; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use std::collections::HashMap; +use url::Url; + +/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive +/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. +/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow +#[derive(Clone, Eq, PartialEq)] +pub struct CodeFlowCredential { + /// Required unless requesting a refresh token + /// The authorization code obtained from a call to authorize. + /// The code should be obtained with all required scopes. + pub(crate) authorization_code: Option<String>, + /// Required when requesting a new access token using a refresh token + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required + /// The application secret that you created in the app registration portal for your app. + /// Don't use the application secret in a native app or single page app because a + /// client_secret can't be reliably stored on devices or web pages. It's required for web + /// apps and web APIs, which can store the client_secret securely on the server side. Like + /// all parameters here, the client secret must be URL-encoded before being sent. This step + /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax + /// specification. The Basic auth pattern of instead providing credentials in the Authorization + /// header, per RFC 6749 is also supported. + pub(crate) client_secret: String, + /// The same redirect_uri value that was used to acquire the authorization_code. + pub(crate) redirect_uri: String, + serializer: OAuth, +} + +impl CodeFlowCredential { + pub fn new<T: AsRef<str>>( + client_id: T, + client_secret: T, + authorization_code: T, + redirect_uri: T, + ) -> CodeFlowCredential { + CodeFlowCredential { + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + serializer: OAuth::new(), + } + } + + pub fn builder() -> CodeFlowCredentialBuilder { + CodeFlowCredentialBuilder::new() + } +} + +impl AuthorizationSerializer for CodeFlowCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + if azure_authority_host.ne(&AzureAuthorityHost::OneDriveAndSharePoint) { + return AuthorizationFailure::required_value_msg_result( + "uri", + Some("Code flow can only be used with AzureAuthorityHost::OneDriveAndSharePoint"), + ); + } + + self.serializer + .authority(azure_authority_host, &Authority::Common); + + if self.refresh_token.is_none() { + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( + "access_token_url", + Some("Internal Error"), + ), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } else { + let uri = self + .serializer + .get(OAuthCredential::RefreshTokenUrl) + .ok_or(AuthorizationFailure::required_value_msg( + "refresh_token_url", + Some("Internal Error"), + ))?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.authorization_code.is_some() && self.refresh_token.is_some() { + return AuthorizationFailure::required_value_msg_result( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Authorization code and refresh token should not be set at the same time - Internal Error"), + ); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + } + + if self.client_secret.trim().is_empty() { + return AuthorizationFailure::required_value_result( + OAuthCredential::ClientSecret.alias(), + ); + } + + if self.redirect_uri.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::RedirectUri); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .redirect_uri(self.redirect_uri.as_str()) + .legacy_authority(); + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result( + OAuthCredential::RefreshToken.alias(), + Some("Either authorization code or refresh token is required"), + ); + } + + self.serializer.refresh_token(refresh_token.as_ref()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ClientSecret), + FormCredential::Required(OAuthCredential::RefreshToken), + FormCredential::Required(OAuthCredential::RedirectUri), + ]); + } else if let Some(authorization_code) = self.authorization_code.as_ref() { + if authorization_code.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result( + OAuthCredential::RefreshToken.alias(), + Some("Either authorization code or refresh token is required"), + ); + } + + self.serializer + .authorization_code(authorization_code.as_ref()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::ClientSecret), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::AuthorizationCode), + ]); + } + + AuthorizationFailure::required_value_msg_result( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Either authorization code or refresh token is required"), + ) + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct CodeFlowCredentialBuilder { + credential: CodeFlowCredential, +} + +impl CodeFlowCredentialBuilder { + fn new() -> CodeFlowCredentialBuilder { + CodeFlowCredentialBuilder { + credential: CodeFlowCredential { + authorization_code: None, + refresh_token: None, + client_id: String::new(), + client_secret: String::new(), + redirect_uri: String::new(), + serializer: Default::default(), + }, + } + } + + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.authorization_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.credential.client_secret = client_secret.as_ref().to_owned(); + self + } + + pub fn build(&self) -> CodeFlowCredential { + self.credential.clone() + } +} + +impl Default for CodeFlowCredentialBuilder { + fn default() -> Self { + CodeFlowCredentialBuilder::new() + } +} diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 46de1b28..656c430e 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -11,8 +11,8 @@ use url::Url; pub struct ConfidentialClientApplication { http_client: reqwest::Client, - credential: Box<dyn AuthorizationSerializer + Send>, token_credential_options: TokenCredentialOptions, + credential: Box<dyn AuthorizationSerializer + Send>, } impl ConfidentialClientApplication { @@ -41,23 +41,21 @@ impl AuthorizationSerializer for ConfidentialClientApplication { #[async_trait] impl TokenRequest for ConfidentialClientApplication { - fn azure_authority_host(&self) -> &AzureAuthorityHost { - &self.token_credential_options.azure_authority_host + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options } fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let uri = self - .credential - .uri(&self.token_credential_options.azure_authority_host)?; + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.credential.uri(&azure_authority_host)?; let form = self.credential.form()?; let http_client = reqwest::blocking::Client::new(); Ok(http_client.post(uri).form(&form).send()?) } async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let uri = self - .credential - .uri(&self.token_credential_options.azure_authority_host)?; + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.credential.uri(&azure_authority_host)?; let form = self.credential.form()?; Ok(self.http_client.post(uri).form(&form).send().await?) } @@ -67,8 +65,8 @@ impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCredential) -> Self { ConfidentialClientApplication { http_client: reqwest::Client::new(), + token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), - token_credential_options: Default::default(), } } } @@ -77,8 +75,8 @@ impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplicat fn from(value: AuthorizationCodeCertificateCredential) -> Self { ConfidentialClientApplication { http_client: reqwest::Client::new(), + token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), - token_credential_options: Default::default(), } } } @@ -87,8 +85,8 @@ impl From<ClientSecretCredential> for ConfidentialClientApplication { fn from(value: ClientSecretCredential) -> Self { ConfidentialClientApplication { http_client: reqwest::Client::new(), + token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), - token_credential_options: Default::default(), } } } @@ -97,8 +95,8 @@ impl From<ClientCertificateCredential> for ConfidentialClientApplication { fn from(value: ClientCertificateCredential) -> Self { ConfidentialClientApplication { http_client: reqwest::Client::new(), + token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), - token_credential_options: Default::default(), } } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs new file mode 100644 index 00000000..8243544a --- /dev/null +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -0,0 +1,242 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::{ + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, +}; +use crate::oauth::form_credential::FormCredential; +use crate::oauth::DeviceCode; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use std::collections::HashMap; +use url::Url; + +static DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; + +/// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, +/// or a printer. To enable this flow, the device has the user visit a webpage in a browser on +/// another device to sign in. Once the user signs in, the device is able to get access tokens +/// and refresh tokens as needed. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code +#[derive(Clone)] +pub struct DeviceCodeCredential { + /// Required when requesting a new access token using a refresh token + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required. + /// The device_code returned in the device authorization request. + /// A device_code is a long string used to verify the session between the client and the authorization server. + /// The client uses this parameter to request the access token from the authorization server. + pub(crate) device_code: Option<String>, + /// A space-separated list of scopes. The scopes must all be from a single resource, + /// along with OIDC scopes (profile, openid, email). For more information, see Permissions + /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension + /// to the authorization code flow, intended to allow apps to declare the resource they want + /// the token for during token redemption. + pub(crate) scope: Vec<String>, + /// The Azure Active Directory tenant (directory) Id of the service principal. + pub(crate) authority: Authority, + pub(crate) token_credential_options: TokenCredentialOptions, + serializer: OAuth, +} + +impl DeviceCodeCredential { + pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + client_id: T, + device_code: T, + scope: I, + ) -> DeviceCodeCredential { + DeviceCodeCredential { + refresh_token: None, + client_id: client_id.as_ref().to_owned(), + device_code: Some(device_code.as_ref().to_owned()), + scope: scope.into_iter().map(|s| s.to_string()).collect(), + authority: Default::default(), + token_credential_options: Default::default(), + serializer: Default::default(), + } + } + + pub fn builder() -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder::new() + } +} + +impl AuthorizationSerializer for DeviceCodeCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + if self.refresh_token.is_none() { + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( + "access_token_url", + Some("Internal Error"), + ), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } else { + let uri = self + .serializer + .get(OAuthCredential::RefreshTokenUrl) + .ok_or(AuthorizationFailure::required_value_msg( + "refresh_token_url", + Some("Internal Error"), + ))?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.device_code.is_some() && self.refresh_token.is_some() { + return AuthorizationFailure::required_value_msg_result( + &format!( + "{} or {}", + OAuthCredential::DeviceCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Device code and refresh token should not be set at the same time - Internal Error"), + ); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + } + + self.serializer + .client_id(self.client_id.as_str()) + .extend_scopes(self.scope.clone()); + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result( + OAuthCredential::RefreshToken.alias(), + Some("Either device code or refresh token is required - found empty refresh token"), + ); + } + + self.serializer + .grant_type("refresh_token") + .device_code(refresh_token.as_ref()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::RefreshToken), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::GrantType), + ]); + } else if let Some(device_code) = self.device_code.as_ref() { + if device_code.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result( + OAuthCredential::DeviceCode.alias(), + Some( + "Either device code or refresh token is required - found empty device code", + ), + ); + } + + self.serializer + .grant_type(DEVICE_CODE_GRANT_TYPE) + .device_code(device_code.as_ref()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::DeviceCode), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::GrantType), + ]); + } + + AuthorizationFailure::required_value_msg_result( + &format!( + "{} or {}", + OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::RefreshToken.alias() + ), + Some("Either authorization code or refresh token is required"), + ) + } +} + +#[derive(Clone)] +pub struct DeviceCodeCredentialBuilder { + credential: DeviceCodeCredential, +} + +impl DeviceCodeCredentialBuilder { + fn new() -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder { + credential: DeviceCodeCredential { + refresh_token: None, + client_id: String::new(), + device_code: None, + scope: vec![], + authority: Default::default(), + token_credential_options: Default::default(), + serializer: Default::default(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_device_code<T: AsRef<str>>(&mut self, device_code: T) -> &mut Self { + self.credential.device_code = Some(device_code.as_ref().to_owned()); + self.credential.refresh_token = None; + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.device_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } + + pub fn build(&self) -> DeviceCodeCredential { + self.credential.clone() + } +} + +impl From<&DeviceCode> for DeviceCodeCredentialBuilder { + fn from(value: &DeviceCode) -> Self { + DeviceCodeCredentialBuilder { + credential: DeviceCodeCredential { + refresh_token: None, + client_id: String::new(), + device_code: Some(value.device_code.clone()), + scope: vec![], + authority: Default::default(), + token_credential_options: Default::default(), + serializer: Default::default(), + }, + } + } +} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 45300ecf..5103c68d 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -4,7 +4,10 @@ mod authorization_code_credential; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; +mod code_flow_authorization_url; +mod code_flow_credential; mod confidential_client_application; +mod device_code_credential; mod implicit_credential_authorization_url; mod prompt; mod proof_key_for_code_exchange; @@ -12,6 +15,7 @@ mod public_client_application; mod response_mode; mod response_type; mod token_credential; +mod token_flow_authorization_url; mod token_request; #[cfg(feature = "openssl")] @@ -23,7 +27,10 @@ pub use authorization_code_credential::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; +pub use code_flow_authorization_url::*; +pub use code_flow_credential::*; pub use confidential_client_application::*; +pub use device_code_credential::*; pub use implicit_credential_authorization_url::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; @@ -31,11 +38,8 @@ pub use public_client_application::*; pub use response_mode::*; pub use response_type::*; pub use token_credential::*; +pub use token_flow_authorization_url::*; pub use token_request::*; #[cfg(feature = "openssl")] pub use client_assertion::*; - -// Powershell -// [System.Diagnostics.Tracing.EventSource]::new("graph-rs-sdk").Guid -pub static EVENT_TRACING_GUID: &str = "58c1e34e-8df1-5dfb-4a3c-6066550ab7f7"; diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index c389c79c..72387bd5 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -22,6 +22,9 @@ pub enum Prompt { /// which would present to the user a list of accounts from which one can be selected for /// authentication. SelectAccount, + /// Use only for federated users. Provides same functionality as prompt=none + /// for managed users. + AttemptNone, } impl AsRef<str> for Prompt { @@ -31,6 +34,7 @@ impl AsRef<str> for Prompt { Prompt::Login => "login", Prompt::Consent => "consent", Prompt::SelectAccount => "select_account", + Prompt::AttemptNone => "attempt_none", } } } diff --git a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs new file mode 100644 index 00000000..a8af1774 --- /dev/null +++ b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs @@ -0,0 +1,114 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::oauth::form_credential::FormCredential; +use crate::oauth::ResponseType; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use url::form_urlencoded::Serializer; +use url::Url; + +/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive +/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. +/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#token-flow +#[derive(Clone)] +pub struct TokenFlowAuthorizationUrl { + pub(crate) client_id: String, + pub(crate) redirect_uri: String, + pub(crate) response_type: ResponseType, + pub(crate) scope: Vec<String>, +} + +impl TokenFlowAuthorizationUrl { + pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + client_id: T, + redirect_uri: T, + scope: I, + ) -> TokenFlowAuthorizationUrl { + TokenFlowAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + response_type: ResponseType::Token, + scope: scope.into_iter().map(|s| s.to_string()).collect(), + } + } + + pub fn builder() -> TokenFlowAuthorizationUrlBuilder { + TokenFlowAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + let mut serializer = OAuth::new(); + if self.redirect_uri.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result("redirect_uri", None); + } + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_msg_result("client_id", None); + } + + if self.scope.is_empty() { + return AuthorizationFailure::required_value_msg_result("scope", None); + } + + serializer + .client_id(self.client_id.as_str()) + .redirect_uri(self.redirect_uri.as_str()) + .extend_scopes(self.scope.clone()) + .legacy_authority() + .response_type(self.response_type.clone()); + + let mut encoder = Serializer::new(String::new()); + serializer.url_query_encode( + vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::RedirectUri), + FormCredential::Required(OAuthCredential::Scope), + FormCredential::Required(OAuthCredential::ResponseType), + ], + &mut encoder, + )?; + + if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) + } else { + AuthorizationFailure::required_value_msg_result( + "authorization_url", + Some("Internal Error"), + ) + } + } +} + +#[derive(Clone)] +pub struct TokenFlowAuthorizationUrlBuilder { + token_flow_authorization_url: TokenFlowAuthorizationUrl, +} + +impl TokenFlowAuthorizationUrlBuilder { + fn new() -> TokenFlowAuthorizationUrlBuilder { + TokenFlowAuthorizationUrlBuilder { + token_flow_authorization_url: TokenFlowAuthorizationUrl { + client_id: String::new(), + redirect_uri: String::new(), + response_type: ResponseType::Token, + scope: vec![], + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.token_flow_authorization_url.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.token_flow_authorization_url.scope = + scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.token_flow_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } +} diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 9b5077d1..301aed2c 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,20 +1,20 @@ -use crate::oauth::{AuthorizationSerializer, AzureAuthorityHost}; +use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; #[async_trait] pub trait TokenRequest: AuthorizationSerializer { - fn azure_authority_host(&self) -> &AzureAuthorityHost; + fn token_credential_options(&self) -> &TokenCredentialOptions; fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.azure_authority_host().clone(); - let uri = self.uri(&azure_authority_host)?; + let options = self.token_credential_options().clone(); + let uri = self.uri(&options.azure_authority_host)?; let form = self.form()?; let http_client = reqwest::blocking::Client::new(); Ok(http_client.post(uri).form(&form).send()?) } async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { - let azure_authority_host = self.azure_authority_host().clone(); - let uri = self.uri(&azure_authority_host)?; + let options = self.token_credential_options().clone(); + let uri = self.uri(&options.azure_authority_host)?; let form = self.form()?; let http_client = reqwest::Client::new(); Ok(http_client.post(uri).form(&form).send().await?) diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 020cc76c..8459e857 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -89,6 +89,7 @@ extern crate serde; mod access_token; mod auth; +mod device_code; mod discovery; mod grants; mod id_token; @@ -96,12 +97,14 @@ pub mod jwt; mod oauth_error; pub mod identity; +pub mod web; pub mod oauth { pub use crate::access_token::AccessToken; pub use crate::auth::GrantSelector; pub use crate::auth::OAuth; pub use crate::auth::OAuthCredential; + pub use crate::device_code::*; pub use crate::discovery::graph_discovery; pub use crate::discovery::jwt_keys; pub use crate::discovery::well_known; diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs new file mode 100644 index 00000000..91663f02 --- /dev/null +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -0,0 +1,68 @@ +use url::{Host, Url}; +use wry::application::window::Theme; +use wry::webview::WebviewExtWindows; +use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, +}; + +pub struct InteractiveWebView; + +impl InteractiveWebView { + fn is_validate_host(uri_to_validate: &Url, validate_against: &Vec<Url>) -> bool { + let hosts: Vec<Host<&str>> = validate_against.iter().flat_map(|uri| uri.host()).collect(); + + if let Some(attempted_host) = uri_to_validate.host() { + hosts.contains(&attempted_host) + } else { + false + } + } + + pub fn interactive_authentication(uri: &Url, redirect_uri: &Url) -> anyhow::Result<()> { + let event_loop: EventLoop<()> = EventLoop::new(); + let valid_uri_vec = vec![uri.clone(), redirect_uri.clone()]; + + let window = WindowBuilder::new() + .with_title("Sign In") + .with_closable(true) + .with_content_protection(true) + .with_minimizable(true) + .with_maximizable(true) + .with_resizable(true) + .with_theme(Some(Theme::Dark)) + .build(&event_loop)?; + + let webview = WebViewBuilder::new(window)? + .with_url(uri.as_ref())? + .with_file_drop_handler(|_, _| { + return true; + }) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + InteractiveWebView::is_validate_host(&url, &valid_uri_vec) + } else { + false + } + }) + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + window_id, + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); + } +} diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs new file mode 100644 index 00000000..7a8571c9 --- /dev/null +++ b/graph-oauth/src/web/mod.rs @@ -0,0 +1,3 @@ +mod interactive_web_view; + +pub use interactive_web_view::*; From e364df4b3937240f870ebdcd1a4ff0b777209b66 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 30 Apr 2023 08:33:55 -0400 Subject: [PATCH 012/118] Add resource owner password credential and helper methods to each credential --- examples/oauth/auth_code_grant.rs | 6 +- examples/oauth/auth_code_grant_pkce.rs | 6 +- examples/oauth/logout.rs | 10 - examples/oauth/main.rs | 1 - graph-oauth/src/access_token.rs | 4 +- graph-oauth/src/id_token.rs | 96 +++++++-- .../src/identity/authorization_serializer.rs | 3 + .../auth_code_authorization_url.rs | 8 +- .../authorization_code_credential.rs | 4 + .../client_certificate_credential.rs | 58 ++++-- .../client_credentials_authorization_url.rs | 21 +- .../credentials/client_secret_credential.rs | 27 ++- .../confidential_client_application.rs | 61 +++++- graph-oauth/src/identity/credentials/mod.rs | 2 + .../credentials/public_client_application.rs | 44 ++++- .../resource_owner_password_credential.rs | 184 ++++++++++++++++++ .../src/identity/credentials/token_request.rs | 40 +++- 17 files changed, 496 insertions(+), 79 deletions(-) delete mode 100644 examples/oauth/logout.rs create mode 100644 graph-oauth/src/identity/credentials/resource_owner_password_credential.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index a3a64a5c..b9272a9e 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -14,13 +14,13 @@ pub struct AccessCode { } pub fn authorization_sign_in() { - let auth_url_builder = AuthCodeAuthorizationUrl::builder() + let url = AuthorizationCodeCredential::authorization_url_builder() .with_client_id(CLIENT_ID) .with_redirect_uri("http://localhost:8000/redirect") .with_scope(vec!["offline_access", "files.read"]) - .build(); + .url() + .unwrap(); - let url = auth_url_builder.url().unwrap(); // web browser crate in dev dependencies will open to default browser in the system. webbrowser::open(url.as_str()).unwrap(); } diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index bd8f7073..c6a1b726 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -32,14 +32,14 @@ pub struct AccessCode { // url and query needed to get an authorization code and opens the default system // web browser to this Url. fn authorization_sign_in() { - let auth_code_url_builder = AuthCodeAuthorizationUrl::builder() + let url = AuthorizationCodeCredential::authorization_url_builder() .with_client_id(CLIENT_ID) .with_scope(vec!["user.read"]) .with_redirect_uri("http://localhost:8000/redirect") .with_proof_key_for_code_exchange(&PKCE) - .build(); + .url() + .unwrap(); - let url = auth_code_url_builder.url().unwrap(); webbrowser::open(url.as_str()).unwrap(); } diff --git a/examples/oauth/logout.rs b/examples/oauth/logout.rs deleted file mode 100644 index a67f1450..00000000 --- a/examples/oauth/logout.rs +++ /dev/null @@ -1,10 +0,0 @@ -use graph_rs_sdk::oauth::OAuth; - -fn logout() { - // First run the example: rocket_example.rs - let mut oauth: OAuth = OAuth::new(); - oauth - .logout_url("https:://localhost:8000/logout") - .post_logout_redirect_uri("https:://localhost:8000/redirect"); - oauth.v1_logout().unwrap(); -} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 2330925e..7719a4ec 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -23,7 +23,6 @@ mod client_credentials_admin_consent; mod device_code; mod implicit_grant; mod is_access_token_expired; -mod logout; mod open_id_connect; mod signing_keys; diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index ac3de6ea..be7a8629 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -28,6 +28,8 @@ use std::str::FromStr; /// For more info see: /// [Microsoft identity platform acccess tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// +/// * Access Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.4 +/// * Refresh Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.5 /// /// For tokens where the JWT can be parsed the elapsed() method uses /// the `exp` field in the JWT's claims. If the claims do not contain an @@ -201,7 +203,7 @@ impl AccessToken { self.id_token = Some(id_token.get_id_token()); } - pub fn parse_id_token(&mut self) -> Option<Result<IdToken, std::io::Error>> { + pub fn parse_id_token(&mut self) -> Option<Result<IdToken, serde_json::Error>> { self.id_token.clone().map(|s| IdToken::from_str(s.as_str())) } diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index 60ca611f..c5d1d39a 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -1,12 +1,16 @@ +use crate::jwt::{JsonWebToken, JwtParser}; +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer}; use serde_json::Value; use std::borrow::Cow; use std::collections::HashMap; use std::convert::TryFrom; +use std::fmt::{Debug, Formatter}; use std::io::ErrorKind; use std::str::FromStr; use url::form_urlencoded; -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Default, Clone, Eq, PartialEq, Serialize)] pub struct IdToken { code: Option<String>, id_token: String, @@ -34,6 +38,10 @@ impl IdToken { self.id_token = id_token.into(); } + pub fn jwt(&self) -> Option<JsonWebToken> { + JwtParser::parse(self.id_token.as_str()).ok() + } + pub fn code(&mut self, code: &str) { self.code = Some(code.into()); } @@ -46,7 +54,11 @@ impl IdToken { self.session_state = Some(session_state.into()); } - pub fn log_pii(&mut self, log_pii: bool) { + /// Enable or disable logging of personally identifiable information such + /// as logging the id_token. This is disabled by default. When log_pii is enabled + /// passing an [IdToken] to logging or print functions will log id_token field. + /// By default this does not get logged. + pub fn enable_pii_logging(&mut self, log_pii: bool) { self.log_pii = log_pii; } @@ -85,16 +97,79 @@ impl TryFrom<&str> for IdToken { } } +impl Debug for IdToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.log_pii { + f.debug_struct("IdToken") + .field("code", &self.code) + .field("id_token", &self.id_token) + .field("session_state", &self.session_state) + .field("additional_fields", &self.additional_fields) + .finish() + } else { + f.debug_struct("IdToken") + .field("code", &self.code) + .field("id_token", &"[REDACTED]") + .field("session_state", &self.session_state) + .field("additional_fields", &self.additional_fields) + .finish() + } + } +} + +struct IdTokenVisitor; + +impl<'de> Deserialize<'de> for IdToken { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + impl<'de> Visitor<'de> for IdTokenVisitor { + type Value = IdToken; + + fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { + formatter.write_str("`code`, `id_token`, `state`, and `session_state`") + } + + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + let vec: Vec<(Cow<str>, Cow<str>)> = form_urlencoded::parse(v).collect(); + + if vec.is_empty() { + return serde_json::from_slice(v) + .map_err(|err| serde::de::Error::custom(err.to_string())); + } + + let mut id_token = IdToken::default(); + for (key, value) in vec.iter() { + match key.as_bytes() { + b"code" => id_token.code(value.as_ref()), + b"id_token" => id_token.id_token(value.as_ref()), + b"state" => id_token.state(value.as_ref()), + b"session_state" => id_token.session_state(value.as_ref()), + _ => { + id_token + .additional_fields + .insert(key.to_string(), Value::String(value.to_string())); + } + } + } + Ok(id_token) + } + } + deserializer.deserialize_identifier(IdTokenVisitor) + } +} + impl FromStr for IdToken { - type Err = std::io::Error; + type Err = serde_json::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { let vec: Vec<(Cow<str>, Cow<str>)> = form_urlencoded::parse(s.as_bytes()).collect(); if vec.is_empty() { - return Err(std::io::Error::new( - ErrorKind::InvalidData, - "Got empty Vec<Cow<str>, Cow<str>> after percent decoding input", - )); + return serde_json::from_slice(s.as_bytes()); } let mut id_token = IdToken::default(); for (key, value) in vec.iter() { @@ -104,10 +179,9 @@ impl FromStr for IdToken { b"state" => id_token.state(value.as_ref()), b"session_state" => id_token.session_state(value.as_ref()), _ => { - return Err(std::io::Error::new( - ErrorKind::InvalidData, - "Invalid key in &str", - )); + id_token + .additional_fields + .insert(key.to_string(), Value::String(value.to_string())); } } } diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index 553b04d5..4f109c58 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -6,4 +6,7 @@ use url::Url; pub trait AuthorizationSerializer { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; fn form(&mut self) -> AuthorizationResult<HashMap<String, String>>; + fn basic_auth(&self) -> Option<(String, String)> { + None + } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 9299feac..a04e2e13 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -57,10 +57,6 @@ impl AuthCodeAuthorizationUrl { } } - pub fn grant_type(&self) -> GrantType { - GrantType::AuthorizationCode - } - pub fn builder() -> AuthCodeAuthorizationUrlBuilder { AuthCodeAuthorizationUrlBuilder::new() } @@ -340,6 +336,10 @@ impl AuthCodeAuthorizationUrlBuilder { pub fn build(&self) -> AuthCodeAuthorizationUrl { self.authorization_code_authorize_url.clone() } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.authorization_code_authorize_url.url() + } } #[cfg(test)] diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 5b42e990..9c7fdf8e 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -212,6 +212,10 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { Some("Either authorization code or refresh token is required"), ) } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } } #[derive(Clone)] diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 785c55a9..ddf16cec 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -12,6 +12,9 @@ use url::Url; use crate::identity::ClientAssertion; use crate::oauth::ClientCredentialsAuthorizationUrlBuilder; +static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #[derive(Clone)] #[allow(dead_code)] pub struct ClientCertificateCredential { @@ -31,6 +34,24 @@ pub struct ClientCertificateCredential { } impl ClientCertificateCredential { + pub fn new<T: AsRef<str>>(client_id: T, client_assertion: T) -> ClientCertificateCredential { + ClientCertificateCredential { + client_id: client_id.as_ref().to_owned(), + scopes: vec![], + authority: Default::default(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: client_assertion.as_ref().to_owned(), + refresh_token: None, + token_credential_options: Default::default(), + serializer: Default::default(), + } + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + pub fn builder() -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder::new() } @@ -52,10 +73,24 @@ impl AuthorizationSerializer for ClientCertificateCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + if self.refresh_token.is_none() { + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( + "access_token_url", + Some("Internal Error"), + ), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } else { + let uri = self + .serializer + .get(OAuthCredential::RefreshTokenUrl) + .ok_or(AuthorizationFailure::required_value_msg( + "refresh_token_url", + Some("Internal Error"), + ))?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } } fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -70,8 +105,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { } if self.client_assertion_type.trim().is_empty() { - self.client_assertion_type = - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_owned(); + self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } self.serializer @@ -128,8 +162,7 @@ impl ClientCertificateCredentialBuilder { client_id: String::new(), scopes: vec![], authority: Default::default(), - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - .to_owned(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), refresh_token: None, token_credential_options: TokenCredentialOptions::default(), @@ -149,7 +182,6 @@ impl ClientCertificateCredentialBuilder { certificate_assertion: &ClientAssertion, ) -> anyhow::Result<&mut Self> { self.with_client_assertion(certificate_assertion.sign()?); - self.with_client_assertion_type("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); Ok(self) } @@ -158,14 +190,6 @@ impl ClientCertificateCredentialBuilder { self } - pub fn with_client_assertion_type<T: AsRef<str>>( - &mut self, - client_assertion_type: T, - ) -> &mut Self { - self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); - self - } - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 593b0058..2d7f88d5 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -83,13 +83,13 @@ impl ClientCredentialsAuthorizationUrl { } pub struct ClientCredentialsAuthorizationUrlBuilder { - credential: ClientCredentialsAuthorizationUrl, + client_credentials_authorization_url: ClientCredentialsAuthorizationUrl, } impl ClientCredentialsAuthorizationUrlBuilder { pub fn new() -> Self { Self { - credential: ClientCredentialsAuthorizationUrl { + client_credentials_authorization_url: ClientCredentialsAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), state: None, @@ -99,32 +99,37 @@ impl ClientCredentialsAuthorizationUrlBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); + self.client_credentials_authorization_url.client_id = client_id.as_ref().to_owned(); self } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); + self.client_credentials_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.client_credentials_authorization_url.authority = + Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); + self.client_credentials_authorization_url.authority = authority.into(); self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.credential.state = Some(state.as_ref().to_owned()); + self.client_credentials_authorization_url.state = Some(state.as_ref().to_owned()); self } pub fn build(&self) -> ClientCredentialsAuthorizationUrl { - self.credential.clone() + self.client_credentials_authorization_url.clone() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.client_credentials_authorization_url.url() } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index be4862a9..dbbce948 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -21,8 +21,19 @@ use url::Url; /// See [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) #[derive(Clone)] pub struct ClientSecretCredential { - /// The client (application) ID of the service principal + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app pub(crate) client_id: String, + /// Required + /// The application secret that you created in the app registration portal for your app. + /// Don't use the application secret in a native app or single page app because a + /// client_secret can't be reliably stored on devices or web pages. It's required for web + /// apps and web APIs, which can store the client_secret securely on the server side. Like + /// all parameters here, the client secret must be URL-encoded before being sent. This step + /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax + /// specification. The Basic auth pattern of instead providing credentials in the Authorization + /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, /// The value passed for the scope parameter in this request should be the resource /// identifier (application ID URI) of the resource you want, affixed with the .default @@ -81,10 +92,7 @@ impl AuthorizationSerializer for ClientSecretCredential { return AuthorizationFailure::required_value_result(OAuthCredential::ClientSecret); } - self.serializer - .client_id(self.client_id.as_str()) - .client_secret(self.client_secret.as_str()) - .grant_type("client_credentials"); + self.serializer.grant_type("client_credentials"); if self.scopes.is_empty() { self.serializer @@ -94,12 +102,15 @@ impl AuthorizationSerializer for ClientSecretCredential { } self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ClientSecret), FormCredential::Required(OAuthCredential::GrantType), FormCredential::NotRequired(OAuthCredential::Scope), ]) } + + /// + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } } pub struct ClientSecretCredentialBuilder { @@ -115,7 +126,7 @@ impl ClientSecretCredentialBuilder { scopes: vec![], authority: Default::default(), token_credential_options: Default::default(), - serializer: OAuth::new(), + serializer: Default::default(), }, } } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 656c430e..791aca96 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -5,10 +5,14 @@ use crate::identity::{ }; use async_trait::async_trait; use graph_error::{AuthorizationResult, GraphResult}; -use reqwest::Response; +use reqwest::tls::Version; +use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; use url::Url; +/// Clients capable of maintaining the confidentiality of their credentials +/// (e.g., client implemented on a secure server with restricted access to the client credentials), +/// or capable of secure client authentication using other means. pub struct ConfidentialClientApplication { http_client: reqwest::Client, token_credential_options: TokenCredentialOptions, @@ -49,22 +53,51 @@ impl TokenRequest for ConfidentialClientApplication { let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); let uri = self.credential.uri(&azure_authority_host)?; let form = self.credential.form()?; - let http_client = reqwest::blocking::Client::new(); - Ok(http_client.post(uri).form(&form).send()?) + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + let basic_auth = self.credential.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send()?) + } else { + Ok(http_client.post(uri).form(&form).send()?) + } } async fn get_token_async(&mut self) -> anyhow::Result<Response> { let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); let uri = self.credential.uri(&azure_authority_host)?; let form = self.credential.form()?; - Ok(self.http_client.post(uri).form(&form).send().await?) + let basic_auth = self.credential.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(self + .http_client + .post(uri) + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send() + .await?) + } else { + Ok(self.http_client.post(uri).form(&form).send().await?) + } } } impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCredential) -> Self { ConfidentialClientApplication { - http_client: reqwest::Client::new(), + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), } @@ -74,7 +107,11 @@ impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCertificateCredential) -> Self { ConfidentialClientApplication { - http_client: reqwest::Client::new(), + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), } @@ -84,7 +121,11 @@ impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplicat impl From<ClientSecretCredential> for ConfidentialClientApplication { fn from(value: ClientSecretCredential) -> Self { ConfidentialClientApplication { - http_client: reqwest::Client::new(), + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), } @@ -94,7 +135,11 @@ impl From<ClientSecretCredential> for ConfidentialClientApplication { impl From<ClientCertificateCredential> for ConfidentialClientApplication { fn from(value: ClientCertificateCredential) -> Self { ConfidentialClientApplication { - http_client: reqwest::Client::new(), + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 5103c68d..cead404b 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -12,6 +12,7 @@ mod implicit_credential_authorization_url; mod prompt; mod proof_key_for_code_exchange; mod public_client_application; +mod resource_owner_password_credential; mod response_mode; mod response_type; mod token_credential; @@ -35,6 +36,7 @@ pub use implicit_credential_authorization_url::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; pub use public_client_application::*; +pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; pub use token_credential::*; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index c5101384..a08484d7 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1 +1,43 @@ -pub struct PublicClientApplication {} +use crate::identity::{ + AuthorizationSerializer, ResourceOwnerPasswordCredential, TokenCredentialOptions, +}; +use reqwest::tls::Version; +use reqwest::ClientBuilder; + +/// Clients incapable of maintaining the confidentiality of their credentials +/// (e.g., clients executing on the device used by the resource owner, such as an +/// installed native application or a web browser-based application), and incapable of +/// secure client authentication via any other means. +pub struct PublicClientApplication { + http_client: reqwest::Client, + token_credential_options: TokenCredentialOptions, + credential: Box<dyn AuthorizationSerializer + Send>, +} + +impl PublicClientApplication { + pub fn new<T>( + credential: T, + options: TokenCredentialOptions, + ) -> anyhow::Result<PublicClientApplication> + where + T: Into<PublicClientApplication>, + { + let mut public_client_application = credential.into(); + public_client_application.token_credential_options = options; + Ok(public_client_application) + } +} + +impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { + fn from(value: ResourceOwnerPasswordCredential) -> Self { + PublicClientApplication { + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), + token_credential_options: value.token_credential_options.clone(), + credential: Box::new(value), + } + } +} diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs new file mode 100644 index 00000000..5c085928 --- /dev/null +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -0,0 +1,184 @@ +use crate::auth::{OAuth, OAuthCredential}; +use crate::identity::form_credential::FormCredential; +use crate::identity::{ + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, +}; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use std::collections::HashMap; +use url::Url; + +/// Allows an application to sign in the user by directly handling their password. +/// Not recommended. ROPC can also be done using a client secret or assertion, +/// however this client implementation does not offer this use case. This is the +/// same as all MSAL clients. +/// https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.3 +#[derive(Clone)] +pub struct ResourceOwnerPasswordCredential { + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required + /// The user's email address. + pub(crate) username: String, + /// Required + /// The user's password. + pub(crate) password: String, + /// The value passed for the scope parameter in this request should be the resource + /// identifier (application ID URI) of the resource you want, affixed with the .default + /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// Default is https://graph.microsoft.com/.default. + pub(crate) scopes: Vec<String>, + pub(crate) authority: Authority, + pub(crate) token_credential_options: TokenCredentialOptions, + serializer: OAuth, +} + +impl ResourceOwnerPasswordCredential { + pub fn new<T: AsRef<str>>( + tenant: T, + client_id: T, + username: T, + password: T, + ) -> ResourceOwnerPasswordCredential { + ResourceOwnerPasswordCredential { + client_id: client_id.as_ref().to_owned(), + username: username.as_ref().to_owned(), + password: password.as_ref().to_owned(), + scopes: vec![], + authority: Authority::TenantId(tenant.as_ref().to_owned()), + token_credential_options: Default::default(), + serializer: Default::default(), + } + } +} + +impl AuthorizationSerializer for ResourceOwnerPasswordCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + )?; + Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.client_id.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + } + + if self.username.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::Username.alias()); + } + + if self.password.trim().is_empty() { + return AuthorizationFailure::required_value_result(OAuthCredential::Password.alias()); + } + + self.serializer + .client_id(self.client_id.as_str()) + .username(self.username.as_str()) + .password(self.password.as_str()) + .grant_type("password") + .extend_scopes(self.scopes.iter()); + + return self.serializer.authorization_form(vec![ + FormCredential::Required(OAuthCredential::ClientId), + FormCredential::Required(OAuthCredential::Username), + FormCredential::Required(OAuthCredential::Password), + FormCredential::Required(OAuthCredential::GrantType), + FormCredential::NotRequired(OAuthCredential::Scope), + ]); + } +} + +#[derive(Clone)] +pub struct ResourceOwnerPasswordCredentialBuilder { + credential: ResourceOwnerPasswordCredential, +} + +impl ResourceOwnerPasswordCredentialBuilder { + pub fn new() -> ResourceOwnerPasswordCredentialBuilder { + ResourceOwnerPasswordCredentialBuilder { + credential: ResourceOwnerPasswordCredential { + client_id: String::new(), + username: String::new(), + password: String::new(), + scopes: vec![], + authority: Authority::Organizations, + token_credential_options: Default::default(), + serializer: Default::default(), + }, + } + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.credential.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_username<T: AsRef<str>>(&mut self, username: T) -> &mut Self { + self.credential.username = username.as_ref().to_owned(); + self + } + + pub fn with_password<T: AsRef<str>>(&mut self, password: T) -> &mut Self { + self.credential.password = password.as_ref().to_owned(); + self + } + + /// The grant type isn't supported on the /common or /consumers authentication contexts. + /// Use /organizations or a tenant ID instead. + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + /// The grant type isn't supported on the /common or /consumers authentication contexts. + /// Use /organizations or a tenant ID instead. + /// Authority defaults to /organizations if no tenant id or authority is given. + pub fn with_authority<T: Into<Authority>>( + &mut self, + authority: T, + ) -> AuthorizationResult<&mut Self> { + let authority = authority.into(); + if authority.eq(&Authority::Common) + || authority.eq(&Authority::AzureActiveDirectory) + || authority.eq(&Authority::Consumers) + { + return AuthorizationFailure::required_value_msg_result( + "tenant_id", + Some("The grant type isn't supported on the /common or /consumers authentication contexts") + ); + } + + self.credential.authority = authority; + Ok(self) + } + + /// Defaults to "https://graph.microsoft.com/.default" + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn with_token_credential_options( + &mut self, + token_credential_options: TokenCredentialOptions, + ) { + self.credential.token_credential_options = token_credential_options; + } + + pub fn build(&self) -> ResourceOwnerPasswordCredential { + self.credential.clone() + } +} + +impl Default for ResourceOwnerPasswordCredentialBuilder { + fn default() -> Self { + ResourceOwnerPasswordCredentialBuilder::new() + } +} diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 301aed2c..0c7d7f97 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,5 +1,7 @@ use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; +use reqwest::tls::Version; +use reqwest::ClientBuilder; #[async_trait] pub trait TokenRequest: AuthorizationSerializer { @@ -9,14 +11,44 @@ pub trait TokenRequest: AuthorizationSerializer { let options = self.token_credential_options().clone(); let uri = self.uri(&options.azure_authority_host)?; let form = self.form()?; - let http_client = reqwest::blocking::Client::new(); - Ok(http_client.post(uri).form(&form).send()?) + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + let basic_auth = self.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send()?) + } else { + Ok(http_client.post(uri).form(&form).send()?) + } } + async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { let options = self.token_credential_options().clone(); let uri = self.uri(&options.azure_authority_host)?; let form = self.form()?; - let http_client = reqwest::Client::new(); - Ok(http_client.post(uri).form(&form).send().await?) + let http_client = ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + let basic_auth = self.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send() + .await?) + } else { + Ok(http_client.post(uri).form(&form).send().await?) + } } } From b2b23c3f6ee7caf2e551a8826b1e34909613b927 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 1 May 2023 03:34:49 -0400 Subject: [PATCH 013/118] Rename resource_api_client to api_client --- Cargo.toml | 2 + examples/oauth/auth_code_grant.rs | 6 - .../src/identity/allowed_host_validator.rs | 0 .../src/web/interactive_web_view_options.rs | 0 src/admin/request.rs | 2 +- src/agreement_acceptances/request.rs | 2 +- src/agreements/request.rs | 2 +- src/app_catalogs/request.rs | 2 +- src/applications/request.rs | 2 +- src/audit_logs/request.rs | 2 +- .../request.rs | 2 +- src/authentication_methods_policy/request.rs | 2 +- src/batch/mod.rs | 2 +- src/branding/request.rs | 2 +- .../request.rs | 2 +- src/chats/chats_messages/request.rs | 2 +- src/chats/chats_messages_replies/request.rs | 2 +- src/chats/request.rs | 2 +- src/client/api_macros/register_client.rs | 8 +- src/client/graph.rs | 172 +++++++++++++++++- src/communications/call_records/request.rs | 2 +- .../call_records_sessions/request.rs | 2 +- src/communications/calls/request.rs | 2 +- src/communications/request.rs | 2 +- src/contracts/request.rs | 2 +- src/data_policy_operations/request.rs | 2 +- .../default_drive_path/request.rs | 2 +- src/default_drive/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../ios_managed_app_protections/request.rs | 2 +- .../managed_app_policies/request.rs | 2 +- .../managed_app_registrations/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../managed_app_statuses/request.rs | 2 +- .../managed_e_books/request.rs | 2 +- .../managed_e_books_device_states/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../mobile_app_categories/request.rs | 2 +- .../mobile_app_configurations/request.rs | 2 +- .../mobile_apps/request.rs | 2 +- src/device_app_management/request.rs | 2 +- .../request.rs | 2 +- .../vpp_tokens/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../device_configurations/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../device_management_reports/request.rs | 2 +- src/device_management/request.rs | 2 +- .../role_definitions/request.rs | 2 +- .../terms_and_conditions/request.rs | 2 +- .../troubleshooting_events/request.rs | 2 +- .../request.rs | 2 +- src/directory/administrative_units/request.rs | 2 +- src/directory/deleted_items/request.rs | 2 +- src/directory/directory_members/request.rs | 2 +- src/directory/request.rs | 2 +- src/directory_objects/request.rs | 2 +- src/directory_role_templates/request.rs | 2 +- src/directory_roles/request.rs | 2 +- src/domain_dns_records/request.rs | 2 +- src/domains/request.rs | 2 +- src/drives/drives_items/request.rs | 2 +- src/drives/drives_items_path/request.rs | 2 +- src/drives/drives_list/request.rs | 2 +- .../drives_list_content_types/request.rs | 2 +- src/drives/request.rs | 2 +- .../education_assignments/request.rs | 2 +- .../request.rs | 2 +- src/education/education_classes/request.rs | 2 +- src/education/education_me/request.rs | 2 +- src/education/education_schools/request.rs | 2 +- src/education/education_users/request.rs | 2 +- src/education/request.rs | 2 +- src/extended_properties/request.rs | 2 +- src/group_lifecycle_policies/request.rs | 2 +- src/groups/conversations/request.rs | 2 +- src/groups/groups_owners/request.rs | 2 +- src/groups/groups_team/request.rs | 2 +- .../members_with_license_errors/request.rs | 2 +- src/groups/request.rs | 2 +- src/groups/threads/request.rs | 2 +- src/groups/threads_posts/request.rs | 2 +- src/groups/transitive_members/request.rs | 2 +- src/identity/request.rs | 2 +- .../request.rs | 2 +- .../access_packages/request.rs | 2 +- .../access_reviews/request.rs | 2 +- .../access_reviews_definitions/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../app_consent/request.rs | 2 +- .../assignment_policies/request.rs | 2 +- .../assignment_requests/request.rs | 2 +- .../connected_organizations/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../entitlement_management/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- src/identity_governance/request.rs | 2 +- src/identity_providers/request.rs | 2 +- src/invitations/request.rs | 2 +- src/me/request.rs | 2 +- src/oauth2_permission_grants/request.rs | 2 +- src/organization/request.rs | 2 +- src/permission_grants/request.rs | 2 +- src/places/request.rs | 2 +- src/planner/buckets/request.rs | 2 +- src/planner/planner_tasks/request.rs | 2 +- src/planner/plans/request.rs | 2 +- src/planner/request.rs | 2 +- src/policies/request.rs | 2 +- src/reports/request.rs | 2 +- src/schema_extensions/request.rs | 2 +- src/service_principals/request.rs | 2 +- .../service_principals_owners/request.rs | 2 +- src/sites/request.rs | 2 +- src/sites/sites_content_types/request.rs | 2 +- src/sites/sites_items/request.rs | 2 +- src/sites/sites_items_versions/request.rs | 2 +- src/sites/sites_lists/request.rs | 2 +- src/sites/term_store/request.rs | 2 +- src/sites/term_store_groups/request.rs | 2 +- src/sites/term_store_sets/request.rs | 2 +- src/sites/term_store_sets_children/request.rs | 2 +- .../term_store_sets_parent_group/request.rs | 2 +- src/sites/term_store_sets_terms/request.rs | 2 +- src/sites/term_stores/request.rs | 2 +- src/subscribed_skus/request.rs | 2 +- src/subscriptions/request.rs | 2 +- src/teams/primary_channel/request.rs | 2 +- src/teams/request.rs | 2 +- src/teams/schedule/request.rs | 2 +- src/teams/shared_with_teams/request.rs | 2 +- src/teams/teams_members/request.rs | 2 +- src/teams/teams_tags/request.rs | 2 +- src/teams_templates/request.rs | 2 +- src/teamwork/deleted_teams/request.rs | 2 +- src/teamwork/request.rs | 2 +- src/users/activities/request.rs | 2 +- src/users/app_role_assignments/request.rs | 2 +- src/users/authentication/request.rs | 2 +- src/users/calendar_groups/request.rs | 2 +- src/users/calendar_view/request.rs | 2 +- src/users/calendars/request.rs | 2 +- src/users/channels/request.rs | 2 +- src/users/child_folders/request.rs | 2 +- src/users/contact_folders/request.rs | 2 +- src/users/contacts/request.rs | 2 +- src/users/created_objects/request.rs | 2 +- src/users/default_calendar/request.rs | 2 +- .../request.rs | 2 +- src/users/direct_reports/request.rs | 2 +- src/users/events/request.rs | 2 +- src/users/events_instances/request.rs | 2 +- src/users/extensions/request.rs | 2 +- src/users/followed_sites/request.rs | 2 +- src/users/inference_classification/request.rs | 2 +- src/users/insights/request.rs | 2 +- src/users/joined_teams/request.rs | 2 +- src/users/license_details/request.rs | 2 +- src/users/mail_folders/request.rs | 2 +- src/users/mailbox_settings/request.rs | 2 +- .../managed_app_registrations/request.rs | 2 +- src/users/managed_devices/request.rs | 2 +- src/users/member_of/request.rs | 2 +- src/users/onenote/request.rs | 2 +- src/users/onenote_notebooks/request.rs | 2 +- src/users/onenote_pages/request.rs | 2 +- src/users/onenote_section_groups/request.rs | 2 +- src/users/onenote_sections/request.rs | 2 +- src/users/online_meetings/request.rs | 2 +- src/users/outlook/request.rs | 2 +- src/users/owned_devices/request.rs | 2 +- src/users/owned_objects/request.rs | 2 +- src/users/photos/request.rs | 2 +- src/users/presence/request.rs | 2 +- src/users/registered_devices/request.rs | 2 +- src/users/request.rs | 2 +- src/users/scoped_role_member_of/request.rs | 2 +- src/users/settings/request.rs | 2 +- src/users/teamwork/request.rs | 2 +- src/users/todo/request.rs | 2 +- src/users/todo_lists/request.rs | 2 +- src/users/todo_lists_tasks/request.rs | 2 +- src/users/transitive_member_of/request.rs | 2 +- src/users/users_attachments/request.rs | 2 +- src/users/users_messages/request.rs | 2 +- tests/download_error.rs | 2 +- 194 files changed, 362 insertions(+), 202 deletions(-) create mode 100644 graph-oauth/src/identity/allowed_host_validator.rs create mode 100644 graph-oauth/src/web/interactive_web_view_options.rs diff --git a/Cargo.toml b/Cargo.toml index f5b56f53..1aac98ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,8 @@ tokio = { version = "1.27.0", features = ["full"] } warp = "0.3.3" webbrowser = "0.8.7" anyhow = "1.0.69" +log = "0.4" +pretty_env_logger = "0.4" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index b9272a9e..373df79b 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -56,12 +56,6 @@ pub async fn start_server_main() { .and(query) .and_then(handle_redirect); - let auth_url_builder = AuthCodeAuthorizationUrl::builder() - .with_client_id(CLIENT_ID) - .with_redirect_uri("http://localhost:8000/redirect") - .with_scope(vec!["offline_access", "files.read"]) - .build(); - authorization_sign_in(); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-oauth/src/web/interactive_web_view_options.rs b/graph-oauth/src/web/interactive_web_view_options.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/admin/request.rs b/src/admin/request.rs index d5d9bca4..585ed796 100644 --- a/src/admin/request.rs +++ b/src/admin/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(AdminApiClient, ResourceIdentity::Admin); +api_client!(AdminApiClient, ResourceIdentity::Admin); impl AdminApiClient { get!( diff --git a/src/agreement_acceptances/request.rs b/src/agreement_acceptances/request.rs index 139d030a..69120269 100644 --- a/src/agreement_acceptances/request.rs +++ b/src/agreement_acceptances/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AgreementAcceptancesApiClient, AgreementAcceptancesIdApiClient, ResourceIdentity::AgreementAcceptances diff --git a/src/agreements/request.rs b/src/agreements/request.rs index 8e2bf180..f2ee149f 100644 --- a/src/agreements/request.rs +++ b/src/agreements/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AgreementsApiClient, AgreementsIdApiClient, ResourceIdentity::Agreements diff --git a/src/app_catalogs/request.rs b/src/app_catalogs/request.rs index d750ac4c..17b659d2 100644 --- a/src/app_catalogs/request.rs +++ b/src/app_catalogs/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(AppCatalogsApiClient, ResourceIdentity::AppCatalogs); +api_client!(AppCatalogsApiClient, ResourceIdentity::AppCatalogs); impl AppCatalogsApiClient { get!( diff --git a/src/applications/request.rs b/src/applications/request.rs index 122a7410..cbf4a24c 100644 --- a/src/applications/request.rs +++ b/src/applications/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::service_principals::*; -resource_api_client!( +api_client!( ApplicationsApiClient, ApplicationsIdApiClient, ResourceIdentity::Applications diff --git a/src/audit_logs/request.rs b/src/audit_logs/request.rs index 3b3682c9..31b01ca6 100644 --- a/src/audit_logs/request.rs +++ b/src/audit_logs/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(AuditLogsApiClient, ResourceIdentity::AuditLogs); +api_client!(AuditLogsApiClient, ResourceIdentity::AuditLogs); impl AuditLogsApiClient { get!( diff --git a/src/authentication_method_configurations/request.rs b/src/authentication_method_configurations/request.rs index 98406dd2..1ef23809 100644 --- a/src/authentication_method_configurations/request.rs +++ b/src/authentication_method_configurations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AuthenticationMethodConfigurationsApiClient, AuthenticationMethodConfigurationsIdApiClient, ResourceIdentity::AuthenticationMethodConfigurations diff --git a/src/authentication_methods_policy/request.rs b/src/authentication_methods_policy/request.rs index 276e8753..67165849 100644 --- a/src/authentication_methods_policy/request.rs +++ b/src/authentication_methods_policy/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::authentication_method_configurations::*; -resource_api_client!( +api_client!( AuthenticationMethodsPolicyApiClient, ResourceIdentity::AuthenticationMethodsPolicy ); diff --git a/src/batch/mod.rs b/src/batch/mod.rs index 6766fa2c..dded077c 100644 --- a/src/batch/mod.rs +++ b/src/batch/mod.rs @@ -1,7 +1,7 @@ use crate::api_default_imports::*; use crate::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -resource_api_client!(BatchApiClient); +api_client!(BatchApiClient); impl BatchApiClient { pub fn batch<B: serde::Serialize>(&self, batch: &B) -> RequestHandler { diff --git a/src/branding/request.rs b/src/branding/request.rs index 3f0b2161..7f70b035 100644 --- a/src/branding/request.rs +++ b/src/branding/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(BrandingApiClient, ResourceIdentity::Branding); +api_client!(BrandingApiClient, ResourceIdentity::Branding); impl BrandingApiClient { get!( diff --git a/src/certificate_based_auth_configuration/request.rs b/src/certificate_based_auth_configuration/request.rs index f320c4ef..6d62aebb 100644 --- a/src/certificate_based_auth_configuration/request.rs +++ b/src/certificate_based_auth_configuration/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( CertificateBasedAuthConfigurationApiClient, CertificateBasedAuthConfigurationIdApiClient, ResourceIdentity::CertificateBasedAuthConfiguration diff --git a/src/chats/chats_messages/request.rs b/src/chats/chats_messages/request.rs index e1e4c2a6..103455a6 100644 --- a/src/chats/chats_messages/request.rs +++ b/src/chats/chats_messages/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::chats::*; -resource_api_client!( +api_client!( ChatsMessagesApiClient, ChatsMessagesIdApiClient, ResourceIdentity::ChatsMessages diff --git a/src/chats/chats_messages_replies/request.rs b/src/chats/chats_messages_replies/request.rs index c065d8ef..f74f2735 100644 --- a/src/chats/chats_messages_replies/request.rs +++ b/src/chats/chats_messages_replies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ChatsMessagesRepliesApiClient, ChatsMessagesRepliesIdApiClient, ResourceIdentity::ChatsMessagesReplies diff --git a/src/chats/request.rs b/src/chats/request.rs index 2548fc90..74a84b39 100644 --- a/src/chats/request.rs +++ b/src/chats/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::chats::*; use crate::teams::*; -resource_api_client!(ChatsApiClient, ChatsIdApiClient, ResourceIdentity::Chats); +api_client!(ChatsApiClient, ChatsIdApiClient, ResourceIdentity::Chats); impl ChatsApiClient { post!( diff --git a/src/client/api_macros/register_client.rs b/src/client/api_macros/register_client.rs index c315b919..d43d6479 100644 --- a/src/client/api_macros/register_client.rs +++ b/src/client/api_macros/register_client.rs @@ -8,7 +8,7 @@ macro_rules! resource_identifier_impl { }; } -macro_rules! resource_api_client { +macro_rules! api_client { ($name:ident) => { pub struct $name { pub(crate) client: graph_http::api_impl::Client, @@ -66,13 +66,13 @@ macro_rules! resource_api_client { }; ($name:ident, $resource_identity:expr) => { - resource_api_client!($name); + api_client!($name); resource_identifier_impl!($name, $resource_identity); }; ($name:ident, $name2:ident, $resource_identity:expr) => { - resource_api_client!($name); - resource_api_client!($name2); + api_client!($name); + api_client!($name2); resource_identifier_impl!($name, $resource_identity); resource_identifier_impl!($name2, $resource_identity); diff --git a/src/client/graph.rs b/src/client/graph.rs index 196d3876..7e86e080 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -65,6 +65,7 @@ use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_error::GraphFailure; use graph_http::api_impl::GraphClientConfiguration; +use graph_oauth::identity::{AllowedHostValidator, HostValidator}; use graph_oauth::oauth::{AccessToken, OAuth}; use lazy_static::lazy_static; use std::convert::TryFrom; @@ -73,6 +74,16 @@ lazy_static! { static ref PARSED_GRAPH_URL: Url = Url::parse(GRAPH_URL).expect("Unable to set v1 endpoint"); static ref PARSED_GRAPH_URL_BETA: Url = Url::parse(GRAPH_URL_BETA).expect("Unable to set beta endpoint"); + static ref VALID_HOSTS: Vec<Url> = vec![ + Url::parse("https://graph.microsoft.com").expect("Unable to parse url for valid host"), + Url::parse("https://graph.microsoft.us").expect("Unable to parse url for valid host"), + Url::parse("https://dod-graph.microsoft.us").expect("Unable to parse url for valid host"), + Url::parse("https://graph.microsoft.de").expect("Unable to parse url for valid host"), + Url::parse("https://microsoftgraph.chinacloudapi.cn") + .expect("Unable to parse url for valid host"), + Url::parse("https://canary.graph.microsoft.com") + .expect("Unable to parse url for valid host") + ]; } #[derive(Debug, Clone)] @@ -171,28 +182,89 @@ impl Graph { } /// Set a custom endpoint for the Microsoft Graph API + /// The scheme must be https:// and any other provided scheme will cause a panic. /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// + /// Attempting to use an invalid host will cause the client to panic. This is done + /// for increased security. + /// + /// Do not use a U.S. Government host endpoint without authorization and any necessary + /// clearances. + /// + /// Using any U.S. Government or China host endpoint means you should + /// expect every API call will be monitored and recorded. The U.S. Government has made it clear + /// you have no right to privacy when using any U.S. Government website or API. + /// + /// You should also assume China's Graph API operated by 21Vianet is being monitored + /// by the Chinese government (who controls all Chinese companies and citizens). + /// And, according to Microsoft, **These services are subject to Chinese laws**. See + /// [Microsoft 365 operated by 21Vianet](https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/microsoft-365-operated-by-21vianet) + /// + /// Valid Hosts: + /// * graph.microsoft.com (Default public endpoint worldwide) + /// * graph.microsoft.us (U.S. Government) + /// * dod-graph.microsoft.us (U.S. Department Of Defense) + /// * graph.microsoft.de + /// * microsoftgraph.chinacloudapi.cn (operated by 21Vianet) + /// * canary.graph.microsoft.com + /// /// Example /// ```rust,ignore /// use graph_rs_sdk::Graph; /// /// let mut client = Graph::new("ACCESS_TOKEN"); /// - /// client.custom_endpoint("https://api.microsoft.com/api") + /// client.custom_endpoint("https://graph.microsoft.com") /// .me() /// .get_user() /// .send() /// .await?; /// ``` pub fn custom_endpoint(&mut self, custom_endpoint: &str) -> &mut Graph { - self.endpoint = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); + match custom_endpoint.validate(&VALID_HOSTS) { + HostValidator::Valid => { + let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); + + if !url.scheme().eq("https") || !url.path().eq("/") || url.query().is_some() { + panic!( + "Invalid path or query - Provide only the host of the Uri such as https://graph.microsoft.com" + ); + } + + self.endpoint = url; + } + HostValidator::Invalid => panic!("Invalid host"), + } self } - /// Set a custom endpoint for the Microsoft Graph API + /// Set a custom endpoint for the Microsoft Graph API. Provide the scheme and host. + /// The scheme must be https:// and any other provided scheme will cause a panic. /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// + /// Attempting to use an invalid host will cause the client to panic. This is done + /// for increased security. + /// + /// Do not use a U.S. Government host endpoint without authorization and any necessary + /// clearances. + /// + /// Using any U.S. Government or China host endpoint means you should + /// expect every API call will be monitored and recorded. The U.S. Government has made it clear + /// you have no right to privacy when using any U.S. Government website or API. + /// + /// You should also assume China's Graph API operated by 21Vianet is being monitored + /// by the Chinese government (who controls all Chinese companies and citizens). + /// And, according to Microsoft, **These services are subject to Chinese laws**. See + /// [Microsoft 365 operated by 21Vianet](https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/microsoft-365-operated-by-21vianet) + /// + /// Valid Hosts: + /// * graph.microsoft.com (Default public endpoint worldwide) + /// * graph.microsoft.us (U.S. Government) + /// * dod-graph.microsoft.us (U.S. Department Of Defense) + /// * graph.microsoft.de + /// * microsoftgraph.chinacloudapi.cn (operated by 21Vianet) + /// * canary.graph.microsoft.com + /// /// Example /// ```rust /// use graph_rs_sdk::Graph; @@ -203,7 +275,20 @@ impl Graph { /// assert_eq!(client.url().to_string(), "https://graph.microsoft.com/".to_string()) /// ``` pub fn use_endpoint(&mut self, custom_endpoint: &str) { - self.endpoint = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); + match custom_endpoint.validate(&VALID_HOSTS) { + HostValidator::Valid => { + let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); + + if !url.scheme().eq("https") || !url.path().eq("/") || url.query().is_some() { + panic!( + "Invalid path query - Provide only the host of the Uri such as https://graph.microsoft.com" + ); + } + + self.endpoint = url; + } + HostValidator::Invalid => panic!("Invalid host"), + } } api_client_impl!(admin, AdminApiClient); @@ -483,3 +568,82 @@ impl From<GraphClientConfiguration> for Graph { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn try_invalid_host() { + let mut client = Graph::new("token"); + client.custom_endpoint("https://example.org"); + } + + #[test] + #[should_panic] + fn try_invalid_scheme() { + let mut client = Graph::new("token"); + client.custom_endpoint("http://example.org"); + } + + #[test] + #[should_panic] + fn try_invalid_query() { + let mut client = Graph::new("token"); + client.custom_endpoint("https://example.org?user=name"); + } + + #[test] + #[should_panic] + fn try_invalid_path() { + let mut client = Graph::new("token"); + client.custom_endpoint("https://example.org/v1"); + } + + #[test] + #[should_panic] + fn try_invalid_host2() { + let mut client = Graph::new("token"); + client.use_endpoint("https://example.org"); + } + + #[test] + #[should_panic] + fn try_invalid_scheme2() { + let mut client = Graph::new("token"); + client.use_endpoint("http://example.org"); + } + + #[test] + #[should_panic] + fn try_invalid_query2() { + let mut client = Graph::new("token"); + client.use_endpoint("https://example.org?user=name"); + } + + #[test] + #[should_panic] + fn try_invalid_path2() { + let mut client = Graph::new("token"); + client.use_endpoint("https://example.org/v1"); + } + + #[test] + fn try_valid_hosts() { + let mut client = Graph::new("token"); + for url in VALID_HOSTS.iter() { + client.custom_endpoint(url.as_str()); + assert_eq!(client.url().host_str(), url.host_str()); + } + } + + #[test] + fn try_valid_hosts2() { + let mut client = Graph::new("token"); + for url in VALID_HOSTS.iter() { + client.use_endpoint(url.as_str()); + assert_eq!(client.url().host_str(), url.host_str()); + } + } +} diff --git a/src/communications/call_records/request.rs b/src/communications/call_records/request.rs index 0cdacdb5..db9852f2 100644 --- a/src/communications/call_records/request.rs +++ b/src/communications/call_records/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::communications::*; -resource_api_client!( +api_client!( CallRecordsApiClient, CallRecordsIdApiClient, ResourceIdentity::CallRecords diff --git a/src/communications/call_records_sessions/request.rs b/src/communications/call_records_sessions/request.rs index 69e3bb0a..708ab9a3 100644 --- a/src/communications/call_records_sessions/request.rs +++ b/src/communications/call_records_sessions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( CallRecordsSessionsApiClient, CallRecordsSessionsIdApiClient, ResourceIdentity::CallRecordsSessions diff --git a/src/communications/calls/request.rs b/src/communications/calls/request.rs index 3671847f..02237eb2 100644 --- a/src/communications/calls/request.rs +++ b/src/communications/calls/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(CallsApiClient, CallsIdApiClient, ResourceIdentity::Calls); +api_client!(CallsApiClient, CallsIdApiClient, ResourceIdentity::Calls); impl CallsApiClient { post!( diff --git a/src/communications/request.rs b/src/communications/request.rs index 4d5bf124..a97ed248 100644 --- a/src/communications/request.rs +++ b/src/communications/request.rs @@ -6,7 +6,7 @@ use crate::communications::{ calls::CallsApiClient, calls::CallsIdApiClient, }; -resource_api_client!(CommunicationsApiClient, ResourceIdentity::Communications); +api_client!(CommunicationsApiClient, ResourceIdentity::Communications); impl CommunicationsApiClient { api_client_link_id!( diff --git a/src/contracts/request.rs b/src/contracts/request.rs index db59de3b..733e4784 100644 --- a/src/contracts/request.rs +++ b/src/contracts/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ContractsApiClient, ContractsIdApiClient, ResourceIdentity::Contracts diff --git a/src/data_policy_operations/request.rs b/src/data_policy_operations/request.rs index 649a9092..dca41772 100644 --- a/src/data_policy_operations/request.rs +++ b/src/data_policy_operations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DataPolicyOperationsApiClient, ResourceIdentity::DataPolicyOperations ); diff --git a/src/default_drive/default_drive_path/request.rs b/src/default_drive/default_drive_path/request.rs index 1b7e5bb2..04281b55 100644 --- a/src/default_drive/default_drive_path/request.rs +++ b/src/default_drive/default_drive_path/request.rs @@ -1,6 +1,6 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DefaultDrivesItemsPathIdApiClient, ResourceIdentity::DrivesItems ); diff --git a/src/default_drive/request.rs b/src/default_drive/request.rs index c829f639..a83f9a5a 100644 --- a/src/default_drive/request.rs +++ b/src/default_drive/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; use crate::default_drive::*; use crate::drives::*; -resource_api_client!(DefaultDriveApiClient, ResourceIdentity::Drive); +api_client!(DefaultDriveApiClient, ResourceIdentity::Drive); impl DefaultDriveApiClient { get!( diff --git a/src/device_app_management/android_managed_app_protections/request.rs b/src/device_app_management/android_managed_app_protections/request.rs index 0f345378..8730c2ee 100644 --- a/src/device_app_management/android_managed_app_protections/request.rs +++ b/src/device_app_management/android_managed_app_protections/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AndroidManagedAppProtectionsApiClient, AndroidManagedAppProtectionsIdApiClient, ResourceIdentity::AndroidManagedAppProtections diff --git a/src/device_app_management/default_managed_app_protections/request.rs b/src/device_app_management/default_managed_app_protections/request.rs index 323dcb13..0edf89f0 100644 --- a/src/device_app_management/default_managed_app_protections/request.rs +++ b/src/device_app_management/default_managed_app_protections/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DefaultManagedAppProtectionsApiClient, DefaultManagedAppProtectionsIdApiClient, ResourceIdentity::DefaultManagedAppProtections diff --git a/src/device_app_management/ios_managed_app_protections/request.rs b/src/device_app_management/ios_managed_app_protections/request.rs index 7d5b41ea..93724eb9 100644 --- a/src/device_app_management/ios_managed_app_protections/request.rs +++ b/src/device_app_management/ios_managed_app_protections/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( IosManagedAppProtectionsApiClient, IosManagedAppProtectionsIdApiClient, ResourceIdentity::IosManagedAppProtections diff --git a/src/device_app_management/managed_app_policies/request.rs b/src/device_app_management/managed_app_policies/request.rs index f806bb12..c5115145 100644 --- a/src/device_app_management/managed_app_policies/request.rs +++ b/src/device_app_management/managed_app_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedAppPoliciesApiClient, ManagedAppPoliciesIdApiClient, ResourceIdentity::ManagedAppPolicies diff --git a/src/device_app_management/managed_app_registrations/request.rs b/src/device_app_management/managed_app_registrations/request.rs index 6de464e9..557e858a 100644 --- a/src/device_app_management/managed_app_registrations/request.rs +++ b/src/device_app_management/managed_app_registrations/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::device_app_management::*; -resource_api_client!( +api_client!( ManagedAppRegistrationsApiClient, ManagedAppRegistrationsIdApiClient, ResourceIdentity::ManagedAppRegistrations diff --git a/src/device_app_management/managed_app_registrations_applied_policies/request.rs b/src/device_app_management/managed_app_registrations_applied_policies/request.rs index 908bbd60..c5eb4fce 100644 --- a/src/device_app_management/managed_app_registrations_applied_policies/request.rs +++ b/src/device_app_management/managed_app_registrations_applied_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedAppRegistrationsAppliedPoliciesApiClient, ManagedAppRegistrationsAppliedPoliciesIdApiClient, ResourceIdentity::ManagedAppRegistrationsAppliedPolicies diff --git a/src/device_app_management/managed_app_registrations_intended_policies/request.rs b/src/device_app_management/managed_app_registrations_intended_policies/request.rs index b6e90686..4cb3af0b 100644 --- a/src/device_app_management/managed_app_registrations_intended_policies/request.rs +++ b/src/device_app_management/managed_app_registrations_intended_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedAppRegistrationsIntendedPoliciesApiClient, ManagedAppRegistrationsIntendedPoliciesIdApiClient, ResourceIdentity::ManagedAppRegistrationsIntendedPolicies diff --git a/src/device_app_management/managed_app_statuses/request.rs b/src/device_app_management/managed_app_statuses/request.rs index 13f89348..78ed76d0 100644 --- a/src/device_app_management/managed_app_statuses/request.rs +++ b/src/device_app_management/managed_app_statuses/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedAppStatusesApiClient, ManagedAppStatusesIdApiClient, ResourceIdentity::ManagedAppStatuses diff --git a/src/device_app_management/managed_e_books/request.rs b/src/device_app_management/managed_e_books/request.rs index 0e1d5697..5d9dce65 100644 --- a/src/device_app_management/managed_e_books/request.rs +++ b/src/device_app_management/managed_e_books/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::device_app_management::*; -resource_api_client!( +api_client!( ManagedEBooksApiClient, ManagedEBooksIdApiClient, ResourceIdentity::ManagedEBooks diff --git a/src/device_app_management/managed_e_books_device_states/request.rs b/src/device_app_management/managed_e_books_device_states/request.rs index ff623a20..95733676 100644 --- a/src/device_app_management/managed_e_books_device_states/request.rs +++ b/src/device_app_management/managed_e_books_device_states/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedEBooksDeviceStatesApiClient, ManagedEBooksDeviceStatesIdApiClient, ResourceIdentity::ManagedEBooksDeviceStates diff --git a/src/device_app_management/managed_e_books_user_state_summary/request.rs b/src/device_app_management/managed_e_books_user_state_summary/request.rs index 97ae359c..5343d3be 100644 --- a/src/device_app_management/managed_e_books_user_state_summary/request.rs +++ b/src/device_app_management/managed_e_books_user_state_summary/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedEBooksUserStateSummaryApiClient, ManagedEBooksUserStateSummaryIdApiClient, ResourceIdentity::ManagedEBooksUserStateSummary diff --git a/src/device_app_management/mdm_windows_information_protection_policies/request.rs b/src/device_app_management/mdm_windows_information_protection_policies/request.rs index 06199e6f..f09749e7 100644 --- a/src/device_app_management/mdm_windows_information_protection_policies/request.rs +++ b/src/device_app_management/mdm_windows_information_protection_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MdmWindowsInformationProtectionPoliciesApiClient, MdmWindowsInformationProtectionPoliciesIdApiClient, ResourceIdentity::MdmWindowsInformationProtectionPolicies diff --git a/src/device_app_management/mobile_app_categories/request.rs b/src/device_app_management/mobile_app_categories/request.rs index 6f66a8c8..e0253487 100644 --- a/src/device_app_management/mobile_app_categories/request.rs +++ b/src/device_app_management/mobile_app_categories/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MobileAppCategoriesApiClient, MobileAppCategoriesIdApiClient, ResourceIdentity::MobileAppCategories diff --git a/src/device_app_management/mobile_app_configurations/request.rs b/src/device_app_management/mobile_app_configurations/request.rs index cb60c199..c42ad694 100644 --- a/src/device_app_management/mobile_app_configurations/request.rs +++ b/src/device_app_management/mobile_app_configurations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MobileAppConfigurationsApiClient, MobileAppConfigurationsIdApiClient, ResourceIdentity::MobileAppConfigurations diff --git a/src/device_app_management/mobile_apps/request.rs b/src/device_app_management/mobile_apps/request.rs index 709ae832..498427c3 100644 --- a/src/device_app_management/mobile_apps/request.rs +++ b/src/device_app_management/mobile_apps/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MobileAppsApiClient, MobileAppsIdApiClient, ResourceIdentity::MobileApps diff --git a/src/device_app_management/request.rs b/src/device_app_management/request.rs index bb3768c8..a04642df 100644 --- a/src/device_app_management/request.rs +++ b/src/device_app_management/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::device_app_management::*; -resource_api_client!( +api_client!( DeviceAppManagementApiClient, ResourceIdentity::DeviceAppManagement ); diff --git a/src/device_app_management/targeted_managed_app_configurations/request.rs b/src/device_app_management/targeted_managed_app_configurations/request.rs index 33991c91..7e3145c6 100644 --- a/src/device_app_management/targeted_managed_app_configurations/request.rs +++ b/src/device_app_management/targeted_managed_app_configurations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TargetedManagedAppConfigurationsApiClient, TargetedManagedAppConfigurationsIdApiClient, ResourceIdentity::TargetedManagedAppConfigurations diff --git a/src/device_app_management/vpp_tokens/request.rs b/src/device_app_management/vpp_tokens/request.rs index b7b20978..2578d6c7 100644 --- a/src/device_app_management/vpp_tokens/request.rs +++ b/src/device_app_management/vpp_tokens/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( VppTokensApiClient, VppTokensIdApiClient, ResourceIdentity::VppTokens diff --git a/src/device_app_management/windows_information_protection_policies/request.rs b/src/device_app_management/windows_information_protection_policies/request.rs index 1c3f6f6a..9a6e52c0 100644 --- a/src/device_app_management/windows_information_protection_policies/request.rs +++ b/src/device_app_management/windows_information_protection_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WindowsInformationProtectionPoliciesApiClient, WindowsInformationProtectionPoliciesIdApiClient, ResourceIdentity::WindowsInformationProtectionPolicies diff --git a/src/device_management/device_compliance_policy_setting_state_summaries/request.rs b/src/device_management/device_compliance_policy_setting_state_summaries/request.rs index 77dd9b3d..0c3bbb94 100644 --- a/src/device_management/device_compliance_policy_setting_state_summaries/request.rs +++ b/src/device_management/device_compliance_policy_setting_state_summaries/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceCompliancePolicySettingStateSummariesApiClient, DeviceCompliancePolicySettingStateSummariesIdApiClient, ResourceIdentity::DeviceCompliancePolicySettingStateSummaries diff --git a/src/device_management/device_configurations/request.rs b/src/device_management/device_configurations/request.rs index db677146..8a00f87f 100644 --- a/src/device_management/device_configurations/request.rs +++ b/src/device_management/device_configurations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceConfigurationsApiClient, DeviceConfigurationsIdApiClient, ResourceIdentity::DeviceConfigurations diff --git a/src/device_management/device_enrollment_configurations/request.rs b/src/device_management/device_enrollment_configurations/request.rs index db651586..ed9c0111 100644 --- a/src/device_management/device_enrollment_configurations/request.rs +++ b/src/device_management/device_enrollment_configurations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceEnrollmentConfigurationsApiClient, DeviceEnrollmentConfigurationsIdApiClient, ResourceIdentity::DeviceEnrollmentConfigurations diff --git a/src/device_management/device_management_managed_devices/request.rs b/src/device_management/device_management_managed_devices/request.rs index 74e95d4a..aeb00c7d 100644 --- a/src/device_management/device_management_managed_devices/request.rs +++ b/src/device_management/device_management_managed_devices/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceManagementManagedDevicesApiClient, DeviceManagementManagedDevicesIdApiClient, ResourceIdentity::DeviceManagementManagedDevices diff --git a/src/device_management/device_management_reports/request.rs b/src/device_management/device_management_reports/request.rs index 7f3b23eb..99dd4f8d 100644 --- a/src/device_management/device_management_reports/request.rs +++ b/src/device_management/device_management_reports/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceManagementReportsApiClient, ResourceIdentity::DeviceManagementReports ); diff --git a/src/device_management/request.rs b/src/device_management/request.rs index b8fba845..122de156 100644 --- a/src/device_management/request.rs +++ b/src/device_management/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::device_management::*; -resource_api_client!( +api_client!( DeviceManagementApiClient, ResourceIdentity::DeviceManagement ); diff --git a/src/device_management/role_definitions/request.rs b/src/device_management/role_definitions/request.rs index 7ebd5a0e..7b075165 100644 --- a/src/device_management/role_definitions/request.rs +++ b/src/device_management/role_definitions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( RoleDefinitionsApiClient, RoleDefinitionsIdApiClient, ResourceIdentity::RoleDefinitions diff --git a/src/device_management/terms_and_conditions/request.rs b/src/device_management/terms_and_conditions/request.rs index a4b22115..be4bbd20 100644 --- a/src/device_management/terms_and_conditions/request.rs +++ b/src/device_management/terms_and_conditions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TermsAndConditionsApiClient, TermsAndConditionsIdApiClient, ResourceIdentity::TermsAndConditions diff --git a/src/device_management/troubleshooting_events/request.rs b/src/device_management/troubleshooting_events/request.rs index 4018ad7f..5ae8d54f 100644 --- a/src/device_management/troubleshooting_events/request.rs +++ b/src/device_management/troubleshooting_events/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TroubleshootingEventsApiClient, TroubleshootingEventsIdApiClient, ResourceIdentity::TroubleshootingEvents diff --git a/src/device_management/windows_autopilot_device_identities/request.rs b/src/device_management/windows_autopilot_device_identities/request.rs index cb26679f..370ba07f 100644 --- a/src/device_management/windows_autopilot_device_identities/request.rs +++ b/src/device_management/windows_autopilot_device_identities/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WindowsAutopilotDeviceIdentitiesApiClient, WindowsAutopilotDeviceIdentitiesIdApiClient, ResourceIdentity::WindowsAutopilotDeviceIdentities diff --git a/src/directory/administrative_units/request.rs b/src/directory/administrative_units/request.rs index e86b99af..73e6d482 100644 --- a/src/directory/administrative_units/request.rs +++ b/src/directory/administrative_units/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::directory::*; -resource_api_client!( +api_client!( AdministrativeUnitsApiClient, AdministrativeUnitsIdApiClient, ResourceIdentity::AdministrativeUnits diff --git a/src/directory/deleted_items/request.rs b/src/directory/deleted_items/request.rs index f08f59ab..cc53766e 100644 --- a/src/directory/deleted_items/request.rs +++ b/src/directory/deleted_items/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeletedItemsApiClient, DeletedItemsIdApiClient, ResourceIdentity::DeletedItems diff --git a/src/directory/directory_members/request.rs b/src/directory/directory_members/request.rs index ecc8396c..72bbddb4 100644 --- a/src/directory/directory_members/request.rs +++ b/src/directory/directory_members/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DirectoryMembersApiClient, DirectoryMembersIdApiClient, ResourceIdentity::DirectoryMembers diff --git a/src/directory/request.rs b/src/directory/request.rs index e6197078..7dc71fb5 100644 --- a/src/directory/request.rs +++ b/src/directory/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::directory::*; -resource_api_client!(DirectoryApiClient, ResourceIdentity::Directory); +api_client!(DirectoryApiClient, ResourceIdentity::Directory); impl DirectoryApiClient { api_client_link!(deleted_items, DeletedItemsApiClient); diff --git a/src/directory_objects/request.rs b/src/directory_objects/request.rs index 319ed473..0683d30c 100644 --- a/src/directory_objects/request.rs +++ b/src/directory_objects/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DirectoryObjectsApiClient, DirectoryObjectsIdApiClient, ResourceIdentity::DirectoryObjects diff --git a/src/directory_role_templates/request.rs b/src/directory_role_templates/request.rs index 93f0ab99..7f75188f 100644 --- a/src/directory_role_templates/request.rs +++ b/src/directory_role_templates/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DirectoryRoleTemplatesApiClient, DirectoryRoleTemplatesIdApiClient, ResourceIdentity::DirectoryRoleTemplates diff --git a/src/directory_roles/request.rs b/src/directory_roles/request.rs index 19108f7e..7232937a 100644 --- a/src/directory_roles/request.rs +++ b/src/directory_roles/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::directory::*; -resource_api_client!( +api_client!( DirectoryRolesApiClient, DirectoryRolesIdApiClient, ResourceIdentity::DirectoryRoles diff --git a/src/domain_dns_records/request.rs b/src/domain_dns_records/request.rs index d98092f8..cdd42567 100644 --- a/src/domain_dns_records/request.rs +++ b/src/domain_dns_records/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DomainDnsRecordsApiClient, DomainDnsRecordsIdApiClient, ResourceIdentity::DomainDnsRecords diff --git a/src/domains/request.rs b/src/domains/request.rs index c3fe5216..8a27dadd 100644 --- a/src/domains/request.rs +++ b/src/domains/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DomainsApiClient, DomainsIdApiClient, ResourceIdentity::Domains diff --git a/src/drives/drives_items/request.rs b/src/drives/drives_items/request.rs index ac089064..5c57305b 100644 --- a/src/drives/drives_items/request.rs +++ b/src/drives/drives_items/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DrivesItemsApiClient, DrivesItemsIdApiClient, ResourceIdentity::DrivesItems diff --git a/src/drives/drives_items_path/request.rs b/src/drives/drives_items_path/request.rs index 59cff64a..9477ec37 100644 --- a/src/drives/drives_items_path/request.rs +++ b/src/drives/drives_items_path/request.rs @@ -1,6 +1,6 @@ use crate::api_default_imports::*; -resource_api_client!(DrivesItemsPathIdApiClient, ResourceIdentity::DrivesItems); +api_client!(DrivesItemsPathIdApiClient, ResourceIdentity::DrivesItems); impl DrivesItemsPathIdApiClient { delete!( diff --git a/src/drives/drives_list/request.rs b/src/drives/drives_list/request.rs index 3516403f..6cc555e3 100644 --- a/src/drives/drives_list/request.rs +++ b/src/drives/drives_list/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!(DrivesListApiClient, ResourceIdentity::DrivesList); +api_client!(DrivesListApiClient, ResourceIdentity::DrivesList); impl DrivesListApiClient { api_client_link_id!(item, DrivesItemsIdApiClient); diff --git a/src/drives/drives_list_content_types/request.rs b/src/drives/drives_list_content_types/request.rs index 1f2ed112..bbc168ac 100644 --- a/src/drives/drives_list_content_types/request.rs +++ b/src/drives/drives_list_content_types/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DrivesListContentTypesApiClient, DrivesListContentTypesIdApiClient, ResourceIdentity::DrivesListContentTypes diff --git a/src/drives/request.rs b/src/drives/request.rs index c0066200..13973cf0 100644 --- a/src/drives/request.rs +++ b/src/drives/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!(DrivesApiClient, DrivesIdApiClient, ResourceIdentity::Drives); +api_client!(DrivesApiClient, DrivesIdApiClient, ResourceIdentity::Drives); impl DrivesApiClient { post!( diff --git a/src/education/education_assignments/request.rs b/src/education/education_assignments/request.rs index 412f5e35..aac34a94 100644 --- a/src/education/education_assignments/request.rs +++ b/src/education/education_assignments/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!( +api_client!( EducationAssignmentsApiClient, EducationAssignmentsIdApiClient, ResourceIdentity::EducationAssignments diff --git a/src/education/education_assignments_submissions/request.rs b/src/education/education_assignments_submissions/request.rs index c5052ad1..86366426 100644 --- a/src/education/education_assignments_submissions/request.rs +++ b/src/education/education_assignments_submissions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( EducationAssignmentsSubmissionsApiClient, EducationAssignmentsSubmissionsIdApiClient, ResourceIdentity::EducationAssignmentsSubmissions diff --git a/src/education/education_classes/request.rs b/src/education/education_classes/request.rs index 8e68ad86..c9403a6a 100644 --- a/src/education/education_classes/request.rs +++ b/src/education/education_classes/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!( +api_client!( EducationClassesApiClient, EducationClassesIdApiClient, ResourceIdentity::EducationClasses diff --git a/src/education/education_me/request.rs b/src/education/education_me/request.rs index 412b0b16..6e36ab38 100644 --- a/src/education/education_me/request.rs +++ b/src/education/education_me/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!(EducationMeApiClient, ResourceIdentity::EducationMe); +api_client!(EducationMeApiClient, ResourceIdentity::EducationMe); impl EducationMeApiClient { api_client_link!(assignments, EducationAssignmentsApiClient); diff --git a/src/education/education_schools/request.rs b/src/education/education_schools/request.rs index 8193114d..ea14f380 100644 --- a/src/education/education_schools/request.rs +++ b/src/education/education_schools/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!( +api_client!( EducationSchoolsApiClient, EducationSchoolsIdApiClient, ResourceIdentity::EducationSchools diff --git a/src/education/education_users/request.rs b/src/education/education_users/request.rs index 3b1954dc..32e37073 100644 --- a/src/education/education_users/request.rs +++ b/src/education/education_users/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!( +api_client!( EducationUsersApiClient, EducationUsersIdApiClient, ResourceIdentity::EducationUsers diff --git a/src/education/request.rs b/src/education/request.rs index afc9ea7f..3ce8c4d6 100644 --- a/src/education/request.rs +++ b/src/education/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::education::*; -resource_api_client!(EducationApiClient, ResourceIdentity::Education); +api_client!(EducationApiClient, ResourceIdentity::Education); impl EducationApiClient { api_client_link!(me, EducationMeApiClient); diff --git a/src/extended_properties/request.rs b/src/extended_properties/request.rs index d7b3798b..597188df 100644 --- a/src/extended_properties/request.rs +++ b/src/extended_properties/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ExtendedPropertiesApiClient, ResourceIdentity::ExtendedProperties ); diff --git a/src/group_lifecycle_policies/request.rs b/src/group_lifecycle_policies/request.rs index 161bb5ac..e56e5d6b 100644 --- a/src/group_lifecycle_policies/request.rs +++ b/src/group_lifecycle_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( GroupLifecyclePoliciesApiClient, GroupLifecyclePoliciesIdApiClient, ResourceIdentity::GroupLifecyclePolicies diff --git a/src/groups/conversations/request.rs b/src/groups/conversations/request.rs index 7ece212b..4ba93dee 100644 --- a/src/groups/conversations/request.rs +++ b/src/groups/conversations/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::groups::*; -resource_api_client!( +api_client!( ConversationsApiClient, ConversationsIdApiClient, ResourceIdentity::Conversations diff --git a/src/groups/groups_owners/request.rs b/src/groups/groups_owners/request.rs index 499dccc0..87bdb0b3 100644 --- a/src/groups/groups_owners/request.rs +++ b/src/groups/groups_owners/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( GroupsOwnersApiClient, GroupsOwnersIdApiClient, ResourceIdentity::GroupsOwners diff --git a/src/groups/groups_team/request.rs b/src/groups/groups_team/request.rs index dba3da36..fc0f32df 100644 --- a/src/groups/groups_team/request.rs +++ b/src/groups/groups_team/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(GroupsTeamApiClient, ResourceIdentity::GroupsTeam); +api_client!(GroupsTeamApiClient, ResourceIdentity::GroupsTeam); impl GroupsTeamApiClient { patch!( diff --git a/src/groups/members_with_license_errors/request.rs b/src/groups/members_with_license_errors/request.rs index 417789a2..6aa5136a 100644 --- a/src/groups/members_with_license_errors/request.rs +++ b/src/groups/members_with_license_errors/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MembersWithLicenseErrorsApiClient, MembersWithLicenseErrorsIdApiClient, ResourceIdentity::MembersWithLicenseErrors diff --git a/src/groups/request.rs b/src/groups/request.rs index b29352f9..f07195c1 100644 --- a/src/groups/request.rs +++ b/src/groups/request.rs @@ -8,7 +8,7 @@ use crate::planner::*; use crate::sites::*; use crate::users::*; -resource_api_client!(GroupsApiClient, GroupsIdApiClient, ResourceIdentity::Groups); +api_client!(GroupsApiClient, GroupsIdApiClient, ResourceIdentity::Groups); impl GroupsApiClient { post!( diff --git a/src/groups/threads/request.rs b/src/groups/threads/request.rs index 9a8d0f3b..0578c572 100644 --- a/src/groups/threads/request.rs +++ b/src/groups/threads/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::groups::*; -resource_api_client!( +api_client!( ThreadsApiClient, ThreadsIdApiClient, ResourceIdentity::Threads diff --git a/src/groups/threads_posts/request.rs b/src/groups/threads_posts/request.rs index 9e57a203..f1fe2eaf 100644 --- a/src/groups/threads_posts/request.rs +++ b/src/groups/threads_posts/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ThreadsPostsApiClient, ThreadsPostsIdApiClient, ResourceIdentity::ThreadsPosts diff --git a/src/groups/transitive_members/request.rs b/src/groups/transitive_members/request.rs index 88cb16da..09c254f7 100644 --- a/src/groups/transitive_members/request.rs +++ b/src/groups/transitive_members/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TransitiveMembersApiClient, TransitiveMembersIdApiClient, ResourceIdentity::TransitiveMembers diff --git a/src/identity/request.rs b/src/identity/request.rs index 1f604d21..2bc80a73 100644 --- a/src/identity/request.rs +++ b/src/identity/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(IdentityApiClient, ResourceIdentity::Identity); +api_client!(IdentityApiClient, ResourceIdentity::Identity); impl IdentityApiClient { get!( diff --git a/src/identity_governance/access_package_assignment_approvals/request.rs b/src/identity_governance/access_package_assignment_approvals/request.rs index 2e5e1955..ad2e3b92 100644 --- a/src/identity_governance/access_package_assignment_approvals/request.rs +++ b/src/identity_governance/access_package_assignment_approvals/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AccessPackageAssignmentApprovalsApiClient, AccessPackageAssignmentApprovalsIdApiClient, ResourceIdentity::AccessPackageAssignmentApprovals diff --git a/src/identity_governance/access_packages/request.rs b/src/identity_governance/access_packages/request.rs index 8af993d9..3ce2b6ba 100644 --- a/src/identity_governance/access_packages/request.rs +++ b/src/identity_governance/access_packages/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::identity_governance::{AssignmentPoliciesApiClient, AssignmentPoliciesIdApiClient}; -resource_api_client!( +api_client!( AccessPackagesApiClient, AccessPackagesIdApiClient, ResourceIdentity::AccessPackages diff --git a/src/identity_governance/access_reviews/request.rs b/src/identity_governance/access_reviews/request.rs index 0a27ed13..002b78d3 100644 --- a/src/identity_governance/access_reviews/request.rs +++ b/src/identity_governance/access_reviews/request.rs @@ -5,7 +5,7 @@ use crate::identity_governance::{ AccessReviewsDefinitionsApiClient, AccessReviewsDefinitionsIdApiClient, }; -resource_api_client!(AccessReviewsApiClient, ResourceIdentity::AccessReviews); +api_client!(AccessReviewsApiClient, ResourceIdentity::AccessReviews); impl AccessReviewsApiClient { api_client_link!(definitions, AccessReviewsDefinitionsApiClient); diff --git a/src/identity_governance/access_reviews_definitions/request.rs b/src/identity_governance/access_reviews_definitions/request.rs index 28d1dac9..a685bbcd 100644 --- a/src/identity_governance/access_reviews_definitions/request.rs +++ b/src/identity_governance/access_reviews_definitions/request.rs @@ -5,7 +5,7 @@ use crate::identity_governance::{ AccessReviewsDefinitionsInstancesApiClient, AccessReviewsDefinitionsInstancesIdApiClient, }; -resource_api_client!( +api_client!( AccessReviewsDefinitionsApiClient, AccessReviewsDefinitionsIdApiClient, ResourceIdentity::AccessReviewsDefinitions diff --git a/src/identity_governance/access_reviews_definitions_instances/request.rs b/src/identity_governance/access_reviews_definitions_instances/request.rs index 81ddd95c..ec7f2e99 100644 --- a/src/identity_governance/access_reviews_definitions_instances/request.rs +++ b/src/identity_governance/access_reviews_definitions_instances/request.rs @@ -6,7 +6,7 @@ use crate::identity_governance::{ AccessReviewsDefinitionsInstancesStagesIdApiClient, }; -resource_api_client!( +api_client!( AccessReviewsDefinitionsInstancesApiClient, AccessReviewsDefinitionsInstancesIdApiClient, ResourceIdentity::AccessReviewsDefinitionsInstances diff --git a/src/identity_governance/access_reviews_definitions_instances_stages/request.rs b/src/identity_governance/access_reviews_definitions_instances_stages/request.rs index 7b5ef59c..d9612579 100644 --- a/src/identity_governance/access_reviews_definitions_instances_stages/request.rs +++ b/src/identity_governance/access_reviews_definitions_instances_stages/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AccessReviewsDefinitionsInstancesStagesApiClient, AccessReviewsDefinitionsInstancesStagesIdApiClient, ResourceIdentity::AccessReviewsDefinitionsInstancesStages diff --git a/src/identity_governance/app_consent/request.rs b/src/identity_governance/app_consent/request.rs index 7424467c..1625bf20 100644 --- a/src/identity_governance/app_consent/request.rs +++ b/src/identity_governance/app_consent/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(AppConsentApiClient, ResourceIdentity::AppConsent); +api_client!(AppConsentApiClient, ResourceIdentity::AppConsent); impl AppConsentApiClient { delete!( diff --git a/src/identity_governance/assignment_policies/request.rs b/src/identity_governance/assignment_policies/request.rs index 749e87f3..2ed5ca81 100644 --- a/src/identity_governance/assignment_policies/request.rs +++ b/src/identity_governance/assignment_policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AssignmentPoliciesApiClient, AssignmentPoliciesIdApiClient, ResourceIdentity::AssignmentPolicies diff --git a/src/identity_governance/assignment_requests/request.rs b/src/identity_governance/assignment_requests/request.rs index 67f472b1..292f9b82 100644 --- a/src/identity_governance/assignment_requests/request.rs +++ b/src/identity_governance/assignment_requests/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AssignmentRequestsApiClient, AssignmentRequestsIdApiClient, ResourceIdentity::AssignmentRequests diff --git a/src/identity_governance/connected_organizations/request.rs b/src/identity_governance/connected_organizations/request.rs index 34949814..3de9627d 100644 --- a/src/identity_governance/connected_organizations/request.rs +++ b/src/identity_governance/connected_organizations/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::identity_governance::*; -resource_api_client!( +api_client!( ConnectedOrganizationsApiClient, ConnectedOrganizationsIdApiClient, ResourceIdentity::ConnectedOrganizations diff --git a/src/identity_governance/connected_organizations_external_sponsors/request.rs b/src/identity_governance/connected_organizations_external_sponsors/request.rs index 4821ed4b..509909ab 100644 --- a/src/identity_governance/connected_organizations_external_sponsors/request.rs +++ b/src/identity_governance/connected_organizations_external_sponsors/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ConnectedOrganizationsExternalSponsorsApiClient, ResourceIdentity::ConnectedOrganizationsExternalSponsors ); diff --git a/src/identity_governance/connected_organizations_internal_sponsors/request.rs b/src/identity_governance/connected_organizations_internal_sponsors/request.rs index 0e10a34d..1f0baf63 100644 --- a/src/identity_governance/connected_organizations_internal_sponsors/request.rs +++ b/src/identity_governance/connected_organizations_internal_sponsors/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ConnectedOrganizationsInternalSponsorsApiClient, ResourceIdentity::ConnectedOrganizationsInternalSponsors ); diff --git a/src/identity_governance/entitlement_management/request.rs b/src/identity_governance/entitlement_management/request.rs index 03b1b19f..273a3b33 100644 --- a/src/identity_governance/entitlement_management/request.rs +++ b/src/identity_governance/entitlement_management/request.rs @@ -15,7 +15,7 @@ use crate::identity_governance::{ EntitlementManagementCatalogsApiClient, EntitlementManagementCatalogsIdApiClient, }; -resource_api_client!( +api_client!( EntitlementManagementApiClient, ResourceIdentity::EntitlementManagement ); diff --git a/src/identity_governance/entitlement_management_assignments/request.rs b/src/identity_governance/entitlement_management_assignments/request.rs index 33ae1b75..893d02b5 100644 --- a/src/identity_governance/entitlement_management_assignments/request.rs +++ b/src/identity_governance/entitlement_management_assignments/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( EntitlementManagementAssignmentsApiClient, EntitlementManagementAssignmentsIdApiClient, ResourceIdentity::EntitlementManagementAssignments diff --git a/src/identity_governance/entitlement_management_catalogs/request.rs b/src/identity_governance/entitlement_management_catalogs/request.rs index 811ea6e0..62558b40 100644 --- a/src/identity_governance/entitlement_management_catalogs/request.rs +++ b/src/identity_governance/entitlement_management_catalogs/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::identity_governance::{AccessPackagesApiClient, AccessPackagesIdApiClient}; -resource_api_client!( +api_client!( EntitlementManagementCatalogsApiClient, EntitlementManagementCatalogsIdApiClient, ResourceIdentity::EntitlementManagementCatalogs diff --git a/src/identity_governance/request.rs b/src/identity_governance/request.rs index 98a856dd..ab405b06 100644 --- a/src/identity_governance/request.rs +++ b/src/identity_governance/request.rs @@ -5,7 +5,7 @@ use crate::identity_governance::{ AccessReviewsApiClient, AppConsentApiClient, EntitlementManagementApiClient, }; -resource_api_client!( +api_client!( IdentityGovernanceApiClient, ResourceIdentity::IdentityGovernance ); diff --git a/src/identity_providers/request.rs b/src/identity_providers/request.rs index 2aa86375..9eb53130 100644 --- a/src/identity_providers/request.rs +++ b/src/identity_providers/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( IdentityProvidersApiClient, IdentityProvidersIdApiClient, ResourceIdentity::IdentityProviders diff --git a/src/invitations/request.rs b/src/invitations/request.rs index c5a67927..67e27a43 100644 --- a/src/invitations/request.rs +++ b/src/invitations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( InvitationsApiClient, InvitationsIdApiClient, ResourceIdentity::Invitations diff --git a/src/me/request.rs b/src/me/request.rs index 4c8c7ea4..a4164698 100644 --- a/src/me/request.rs +++ b/src/me/request.rs @@ -9,7 +9,7 @@ use crate::planner::*; use crate::teams::*; use crate::users::*; -resource_api_client!(MeApiClient, ResourceIdentity::Me); +api_client!(MeApiClient, ResourceIdentity::Me); impl MeApiClient { api_client_link_id!(message, UsersMessagesIdApiClient); diff --git a/src/oauth2_permission_grants/request.rs b/src/oauth2_permission_grants/request.rs index 41408550..3568d10c 100644 --- a/src/oauth2_permission_grants/request.rs +++ b/src/oauth2_permission_grants/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, ResourceIdentity::Oauth2PermissionGrants diff --git a/src/organization/request.rs b/src/organization/request.rs index 511fb873..85586ea5 100644 --- a/src/organization/request.rs +++ b/src/organization/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( OrganizationApiClient, OrganizationIdApiClient, ResourceIdentity::Organization diff --git a/src/permission_grants/request.rs b/src/permission_grants/request.rs index a12b7e6d..693b17a6 100644 --- a/src/permission_grants/request.rs +++ b/src/permission_grants/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( PermissionGrantsApiClient, PermissionGrantsIdApiClient, ResourceIdentity::PermissionGrants diff --git a/src/places/request.rs b/src/places/request.rs index 3130bec7..502e5324 100644 --- a/src/places/request.rs +++ b/src/places/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(PlacesApiClient, PlacesIdApiClient, ResourceIdentity::Places); +api_client!(PlacesApiClient, PlacesIdApiClient, ResourceIdentity::Places); impl PlacesApiClient { get!( diff --git a/src/planner/buckets/request.rs b/src/planner/buckets/request.rs index 29b1bf37..d9a5018d 100644 --- a/src/planner/buckets/request.rs +++ b/src/planner/buckets/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::planner::*; -resource_api_client!( +api_client!( BucketsApiClient, BucketsIdApiClient, ResourceIdentity::Buckets diff --git a/src/planner/planner_tasks/request.rs b/src/planner/planner_tasks/request.rs index be9bf8b8..b3ef05f8 100644 --- a/src/planner/planner_tasks/request.rs +++ b/src/planner/planner_tasks/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( PlannerTasksApiClient, PlannerTasksIdApiClient, ResourceIdentity::PlannerTasks diff --git a/src/planner/plans/request.rs b/src/planner/plans/request.rs index 9fa59d31..614dfabc 100644 --- a/src/planner/plans/request.rs +++ b/src/planner/plans/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::planner::*; -resource_api_client!(PlansApiClient, PlansIdApiClient, ResourceIdentity::Plans); +api_client!(PlansApiClient, PlansIdApiClient, ResourceIdentity::Plans); impl PlansApiClient { post!( diff --git a/src/planner/request.rs b/src/planner/request.rs index 781fa770..ea7eb328 100644 --- a/src/planner/request.rs +++ b/src/planner/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::planner::*; -resource_api_client!(PlannerApiClient, ResourceIdentity::Planner); +api_client!(PlannerApiClient, ResourceIdentity::Planner); impl PlannerApiClient { api_client_link_id!(bucket, BucketsIdApiClient); diff --git a/src/policies/request.rs b/src/policies/request.rs index c1ba12e7..5fe726c7 100644 --- a/src/policies/request.rs +++ b/src/policies/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(PoliciesApiClient, ResourceIdentity::Policies); +api_client!(PoliciesApiClient, ResourceIdentity::Policies); impl PoliciesApiClient { get!( diff --git a/src/reports/request.rs b/src/reports/request.rs index 1363de5d..ad4f04c8 100644 --- a/src/reports/request.rs +++ b/src/reports/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(ReportsApiClient, ResourceIdentity::Reports); +api_client!(ReportsApiClient, ResourceIdentity::Reports); impl ReportsApiClient { get!( diff --git a/src/schema_extensions/request.rs b/src/schema_extensions/request.rs index 07539368..6094908e 100644 --- a/src/schema_extensions/request.rs +++ b/src/schema_extensions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SchemaExtensionsApiClient, SchemaExtensionsIdApiClient, ResourceIdentity::SchemaExtensions diff --git a/src/service_principals/request.rs b/src/service_principals/request.rs index 0d7f2fe0..4faa0d08 100644 --- a/src/service_principals/request.rs +++ b/src/service_principals/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::service_principals::*; use crate::users::*; -resource_api_client!( +api_client!( ServicePrincipalsApiClient, ServicePrincipalsIdApiClient, ResourceIdentity::ServicePrincipals diff --git a/src/service_principals/service_principals_owners/request.rs b/src/service_principals/service_principals_owners/request.rs index acceb6b9..5aee9423 100644 --- a/src/service_principals/service_principals_owners/request.rs +++ b/src/service_principals/service_principals_owners/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ServicePrincipalsOwnersApiClient, ServicePrincipalsOwnersIdApiClient, ResourceIdentity::ServicePrincipalsOwners diff --git a/src/sites/request.rs b/src/sites/request.rs index a349941a..f1f8ddc9 100644 --- a/src/sites/request.rs +++ b/src/sites/request.rs @@ -5,7 +5,7 @@ use crate::default_drive::*; use crate::sites::*; use crate::users::*; -resource_api_client!(SitesApiClient, SitesIdApiClient, ResourceIdentity::Sites); +api_client!(SitesApiClient, SitesIdApiClient, ResourceIdentity::Sites); impl SitesApiClient { get!( diff --git a/src/sites/sites_content_types/request.rs b/src/sites/sites_content_types/request.rs index 197c3146..662e909c 100644 --- a/src/sites/sites_content_types/request.rs +++ b/src/sites/sites_content_types/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SitesContentTypesApiClient, SitesContentTypesIdApiClient, ResourceIdentity::SitesContentTypes diff --git a/src/sites/sites_items/request.rs b/src/sites/sites_items/request.rs index 59cd346c..dd73874a 100644 --- a/src/sites/sites_items/request.rs +++ b/src/sites/sites_items/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!( +api_client!( SitesItemsApiClient, SitesItemsIdApiClient, ResourceIdentity::SitesItems diff --git a/src/sites/sites_items_versions/request.rs b/src/sites/sites_items_versions/request.rs index 1a22e4fb..6a542373 100644 --- a/src/sites/sites_items_versions/request.rs +++ b/src/sites/sites_items_versions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SitesItemsVersionsApiClient, SitesItemsVersionsIdApiClient, ResourceIdentity::SitesItemsVersions diff --git a/src/sites/sites_lists/request.rs b/src/sites/sites_lists/request.rs index 66a06159..4ecd006c 100644 --- a/src/sites/sites_lists/request.rs +++ b/src/sites/sites_lists/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!( +api_client!( SitesListsApiClient, SitesListsIdApiClient, ResourceIdentity::SitesLists diff --git a/src/sites/term_store/request.rs b/src/sites/term_store/request.rs index f3bc2aff..76402a04 100644 --- a/src/sites/term_store/request.rs +++ b/src/sites/term_store/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!(TermStoreApiClient, ResourceIdentity::TermStore); +api_client!(TermStoreApiClient, ResourceIdentity::TermStore); impl TermStoreApiClient { api_client_link_id!(set, TermStoreSetsIdApiClient); diff --git a/src/sites/term_store_groups/request.rs b/src/sites/term_store_groups/request.rs index bed5b035..9c08ab48 100644 --- a/src/sites/term_store_groups/request.rs +++ b/src/sites/term_store_groups/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TermStoreGroupsApiClient, TermStoreGroupsIdApiClient, ResourceIdentity::TermStoreGroups diff --git a/src/sites/term_store_sets/request.rs b/src/sites/term_store_sets/request.rs index 4471da12..94f5bac6 100644 --- a/src/sites/term_store_sets/request.rs +++ b/src/sites/term_store_sets/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!( +api_client!( TermStoreSetsApiClient, TermStoreSetsIdApiClient, ResourceIdentity::TermStoreSets diff --git a/src/sites/term_store_sets_children/request.rs b/src/sites/term_store_sets_children/request.rs index b729a38c..025b3a4f 100644 --- a/src/sites/term_store_sets_children/request.rs +++ b/src/sites/term_store_sets_children/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TermStoreSetsChildrenApiClient, TermStoreSetsChildrenIdApiClient, ResourceIdentity::TermStoreSetsChildren diff --git a/src/sites/term_store_sets_parent_group/request.rs b/src/sites/term_store_sets_parent_group/request.rs index accece4d..4fbcdced 100644 --- a/src/sites/term_store_sets_parent_group/request.rs +++ b/src/sites/term_store_sets_parent_group/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!( +api_client!( TermStoreSetsParentGroupApiClient, ResourceIdentity::TermStoreSetsParentGroup ); diff --git a/src/sites/term_store_sets_terms/request.rs b/src/sites/term_store_sets_terms/request.rs index 440dc16c..d649cac9 100644 --- a/src/sites/term_store_sets_terms/request.rs +++ b/src/sites/term_store_sets_terms/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TermStoreSetsTermsApiClient, TermStoreSetsTermsIdApiClient, ResourceIdentity::TermStoreSetsTerms diff --git a/src/sites/term_stores/request.rs b/src/sites/term_stores/request.rs index 123c2253..daa96400 100644 --- a/src/sites/term_stores/request.rs +++ b/src/sites/term_stores/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::sites::*; -resource_api_client!( +api_client!( TermStoresApiClient, TermStoresIdApiClient, ResourceIdentity::TermStores diff --git a/src/subscribed_skus/request.rs b/src/subscribed_skus/request.rs index 36f1624b..ac629d18 100644 --- a/src/subscribed_skus/request.rs +++ b/src/subscribed_skus/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SubscribedSkusApiClient, SubscribedSkusIdApiClient, ResourceIdentity::SubscribedSkus diff --git a/src/subscriptions/request.rs b/src/subscriptions/request.rs index 8461cc0c..08ec3313 100644 --- a/src/subscriptions/request.rs +++ b/src/subscriptions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SubscriptionsApiClient, SubscriptionsIdApiClient, ResourceIdentity::Subscriptions diff --git a/src/teams/primary_channel/request.rs b/src/teams/primary_channel/request.rs index dac92708..591c3f69 100644 --- a/src/teams/primary_channel/request.rs +++ b/src/teams/primary_channel/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::chats::*; use crate::teams::*; -resource_api_client!(PrimaryChannelApiClient, ResourceIdentity::PrimaryChannel); +api_client!(PrimaryChannelApiClient, ResourceIdentity::PrimaryChannel); impl PrimaryChannelApiClient { api_client_link!(members, TeamsMembersApiClient); diff --git a/src/teams/request.rs b/src/teams/request.rs index bd686561..3b81e5d6 100644 --- a/src/teams/request.rs +++ b/src/teams/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::teams::*; use crate::users::*; -resource_api_client!(TeamsApiClient, TeamsIdApiClient, ResourceIdentity::Teams); +api_client!(TeamsApiClient, TeamsIdApiClient, ResourceIdentity::Teams); impl TeamsApiClient { post!( diff --git a/src/teams/schedule/request.rs b/src/teams/schedule/request.rs index eb179377..b4abb0a9 100644 --- a/src/teams/schedule/request.rs +++ b/src/teams/schedule/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(ScheduleApiClient, ResourceIdentity::Schedule); +api_client!(ScheduleApiClient, ResourceIdentity::Schedule); impl ScheduleApiClient { delete!( diff --git a/src/teams/shared_with_teams/request.rs b/src/teams/shared_with_teams/request.rs index 6defd55c..0eedda12 100644 --- a/src/teams/shared_with_teams/request.rs +++ b/src/teams/shared_with_teams/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( SharedWithTeamsApiClient, SharedWithTeamsIdApiClient, ResourceIdentity::SharedWithTeams diff --git a/src/teams/teams_members/request.rs b/src/teams/teams_members/request.rs index d604190c..ac246f26 100644 --- a/src/teams/teams_members/request.rs +++ b/src/teams/teams_members/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TeamsMembersApiClient, TeamsMembersIdApiClient, ResourceIdentity::TeamsMembers diff --git a/src/teams/teams_tags/request.rs b/src/teams/teams_tags/request.rs index 20fa4eef..37ac75af 100644 --- a/src/teams/teams_tags/request.rs +++ b/src/teams/teams_tags/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::teams::*; -resource_api_client!( +api_client!( TeamsTagsApiClient, TeamsTagsIdApiClient, ResourceIdentity::TeamsTags diff --git a/src/teams_templates/request.rs b/src/teams_templates/request.rs index 5e98bc57..43d7d0fb 100644 --- a/src/teams_templates/request.rs +++ b/src/teams_templates/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TeamsTemplatesApiClient, TeamsTemplatesIdApiClient, ResourceIdentity::TeamsTemplates diff --git a/src/teamwork/deleted_teams/request.rs b/src/teamwork/deleted_teams/request.rs index d0ff83bb..a41d96c6 100644 --- a/src/teamwork/deleted_teams/request.rs +++ b/src/teamwork/deleted_teams/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( DeletedTeamsApiClient, DeletedTeamsIdApiClient, ResourceIdentity::DeletedTeams diff --git a/src/teamwork/request.rs b/src/teamwork/request.rs index 9f473cea..062cd377 100644 --- a/src/teamwork/request.rs +++ b/src/teamwork/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::teamwork::*; -resource_api_client!(TeamworkApiClient, ResourceIdentity::Teamwork); +api_client!(TeamworkApiClient, ResourceIdentity::Teamwork); impl TeamworkApiClient { api_client_link_id!(deleted_team, DeletedTeamsIdApiClient); diff --git a/src/users/activities/request.rs b/src/users/activities/request.rs index c06de590..bf653062 100644 --- a/src/users/activities/request.rs +++ b/src/users/activities/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ActivitiesApiClient, ActivitiesIdApiClient, ResourceIdentity::Activities diff --git a/src/users/app_role_assignments/request.rs b/src/users/app_role_assignments/request.rs index 97214a8d..a9ed60a4 100644 --- a/src/users/app_role_assignments/request.rs +++ b/src/users/app_role_assignments/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AppRoleAssignmentsApiClient, AppRoleAssignmentsIdApiClient, ResourceIdentity::AppRoleAssignments diff --git a/src/users/authentication/request.rs b/src/users/authentication/request.rs index 765e08aa..b27cebdd 100644 --- a/src/users/authentication/request.rs +++ b/src/users/authentication/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(AuthenticationApiClient, ResourceIdentity::Authentication); +api_client!(AuthenticationApiClient, ResourceIdentity::Authentication); impl AuthenticationApiClient { delete!( diff --git a/src/users/calendar_groups/request.rs b/src/users/calendar_groups/request.rs index a2e43108..b704db82 100644 --- a/src/users/calendar_groups/request.rs +++ b/src/users/calendar_groups/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( CalendarGroupsApiClient, CalendarGroupsIdApiClient, ResourceIdentity::CalendarGroups diff --git a/src/users/calendar_view/request.rs b/src/users/calendar_view/request.rs index 4954f843..12e93083 100644 --- a/src/users/calendar_view/request.rs +++ b/src/users/calendar_view/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( CalendarViewApiClient, CalendarViewIdApiClient, ResourceIdentity::CalendarView diff --git a/src/users/calendars/request.rs b/src/users/calendars/request.rs index 707e16e5..7fffb4cb 100644 --- a/src/users/calendars/request.rs +++ b/src/users/calendars/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::extended_properties::*; use crate::users::*; -resource_api_client!( +api_client!( CalendarsApiClient, CalendarsIdApiClient, ResourceIdentity::Calendars diff --git a/src/users/channels/request.rs b/src/users/channels/request.rs index 6277ae75..1d45af70 100644 --- a/src/users/channels/request.rs +++ b/src/users/channels/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::chats::*; use crate::teams::*; -resource_api_client!( +api_client!( ChannelsApiClient, ChannelsIdApiClient, ResourceIdentity::Channels diff --git a/src/users/child_folders/request.rs b/src/users/child_folders/request.rs index 5e3125e6..46bf4794 100644 --- a/src/users/child_folders/request.rs +++ b/src/users/child_folders/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::extended_properties::*; use crate::users::*; -resource_api_client!( +api_client!( ChildFoldersApiClient, ChildFoldersIdApiClient, ResourceIdentity::ChildFolders diff --git a/src/users/contact_folders/request.rs b/src/users/contact_folders/request.rs index 61688569..e5f5dd4e 100644 --- a/src/users/contact_folders/request.rs +++ b/src/users/contact_folders/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::extended_properties::*; use crate::users::*; -resource_api_client!( +api_client!( ContactFoldersApiClient, ContactFoldersIdApiClient, ResourceIdentity::ContactFolders diff --git a/src/users/contacts/request.rs b/src/users/contacts/request.rs index 264f5f29..102d0d7c 100644 --- a/src/users/contacts/request.rs +++ b/src/users/contacts/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ContactsApiClient, ContactsIdApiClient, ResourceIdentity::Contacts diff --git a/src/users/created_objects/request.rs b/src/users/created_objects/request.rs index 9ed88a8b..3a0856e5 100644 --- a/src/users/created_objects/request.rs +++ b/src/users/created_objects/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( CreatedObjectsApiClient, CreatedObjectsIdApiClient, ResourceIdentity::CreatedObjects diff --git a/src/users/default_calendar/request.rs b/src/users/default_calendar/request.rs index 7ce934bb..7360595d 100644 --- a/src/users/default_calendar/request.rs +++ b/src/users/default_calendar/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::extended_properties::*; use crate::users::*; -resource_api_client!(DefaultCalendarApiClient, ResourceIdentity::DefaultCalendar); +api_client!(DefaultCalendarApiClient, ResourceIdentity::DefaultCalendar); impl DefaultCalendarApiClient { api_client_link_id!(event, EventsIdApiClient); diff --git a/src/users/device_management_troubleshooting_events/request.rs b/src/users/device_management_troubleshooting_events/request.rs index 38ab31ef..c4aca4bc 100644 --- a/src/users/device_management_troubleshooting_events/request.rs +++ b/src/users/device_management_troubleshooting_events/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DeviceManagementTroubleshootingEventsApiClient, DeviceManagementTroubleshootingEventsIdApiClient, ResourceIdentity::TroubleshootingEvents diff --git a/src/users/direct_reports/request.rs b/src/users/direct_reports/request.rs index 14bcc88e..4491830c 100644 --- a/src/users/direct_reports/request.rs +++ b/src/users/direct_reports/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( DirectReportsApiClient, DirectReportsIdApiClient, ResourceIdentity::DirectReports diff --git a/src/users/events/request.rs b/src/users/events/request.rs index 9cfb7ea5..760fc899 100644 --- a/src/users/events/request.rs +++ b/src/users/events/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!(EventsApiClient, EventsIdApiClient, ResourceIdentity::Events); +api_client!(EventsApiClient, EventsIdApiClient, ResourceIdentity::Events); impl EventsApiClient { post!( diff --git a/src/users/events_instances/request.rs b/src/users/events_instances/request.rs index 9e84ef75..b52bc8d6 100644 --- a/src/users/events_instances/request.rs +++ b/src/users/events_instances/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( EventsInstancesApiClient, EventsInstancesIdApiClient, ResourceIdentity::EventsInstances diff --git a/src/users/extensions/request.rs b/src/users/extensions/request.rs index eb9ded91..66233d96 100644 --- a/src/users/extensions/request.rs +++ b/src/users/extensions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ExtensionsApiClient, ExtensionsIdApiClient, ResourceIdentity::Extensions diff --git a/src/users/followed_sites/request.rs b/src/users/followed_sites/request.rs index a6885a3c..76aed344 100644 --- a/src/users/followed_sites/request.rs +++ b/src/users/followed_sites/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( FollowedSitesApiClient, FollowedSitesIdApiClient, ResourceIdentity::FollowedSites diff --git a/src/users/inference_classification/request.rs b/src/users/inference_classification/request.rs index 0e33fb2e..4e28e269 100644 --- a/src/users/inference_classification/request.rs +++ b/src/users/inference_classification/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( InferenceClassificationApiClient, ResourceIdentity::InferenceClassification ); diff --git a/src/users/insights/request.rs b/src/users/insights/request.rs index be32f23e..1d464c1b 100644 --- a/src/users/insights/request.rs +++ b/src/users/insights/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(InsightsApiClient, ResourceIdentity::Insights); +api_client!(InsightsApiClient, ResourceIdentity::Insights); impl InsightsApiClient { delete!( diff --git a/src/users/joined_teams/request.rs b/src/users/joined_teams/request.rs index 552a3327..a8b3f070 100644 --- a/src/users/joined_teams/request.rs +++ b/src/users/joined_teams/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::teams::*; use crate::users::*; -resource_api_client!( +api_client!( JoinedTeamsApiClient, JoinedTeamsIdApiClient, ResourceIdentity::JoinedTeams diff --git a/src/users/license_details/request.rs b/src/users/license_details/request.rs index aa5e65f2..44c46a9c 100644 --- a/src/users/license_details/request.rs +++ b/src/users/license_details/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( LicenseDetailsApiClient, LicenseDetailsIdApiClient, ResourceIdentity::LicenseDetails diff --git a/src/users/mail_folders/request.rs b/src/users/mail_folders/request.rs index 8710c690..d805cbc5 100644 --- a/src/users/mail_folders/request.rs +++ b/src/users/mail_folders/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::extended_properties::*; use crate::users::*; -resource_api_client!( +api_client!( MailFoldersApiClient, MailFoldersIdApiClient, ResourceIdentity::MailFolders diff --git a/src/users/mailbox_settings/request.rs b/src/users/mailbox_settings/request.rs index 36908641..bee0bc6b 100644 --- a/src/users/mailbox_settings/request.rs +++ b/src/users/mailbox_settings/request.rs @@ -1,6 +1,6 @@ use crate::api_default_imports::*; -resource_api_client!(MailboxSettingsApiClient, ResourceIdentity::MailboxSettings); +api_client!(MailboxSettingsApiClient, ResourceIdentity::MailboxSettings); impl MailboxSettingsApiClient { get!( diff --git a/src/users/managed_app_registrations/request.rs b/src/users/managed_app_registrations/request.rs index f1baf5a8..30dc1c12 100644 --- a/src/users/managed_app_registrations/request.rs +++ b/src/users/managed_app_registrations/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedAppRegistrationsApiClient, ManagedAppRegistrationsIdApiClient, ResourceIdentity::ManagedAppRegistrations diff --git a/src/users/managed_devices/request.rs b/src/users/managed_devices/request.rs index f3a38591..2bef465d 100644 --- a/src/users/managed_devices/request.rs +++ b/src/users/managed_devices/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ManagedDevicesApiClient, ManagedDevicesIdApiClient, ResourceIdentity::UsersManagedDevices diff --git a/src/users/member_of/request.rs b/src/users/member_of/request.rs index 88cd9817..85e47fc7 100644 --- a/src/users/member_of/request.rs +++ b/src/users/member_of/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( MemberOfApiClient, MemberOfIdApiClient, ResourceIdentity::MemberOf diff --git a/src/users/onenote/request.rs b/src/users/onenote/request.rs index 44091e2e..6a2b452b 100644 --- a/src/users/onenote/request.rs +++ b/src/users/onenote/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!(OnenoteApiClient, ResourceIdentity::Onenote); +api_client!(OnenoteApiClient, ResourceIdentity::Onenote); impl OnenoteApiClient { api_client_link!(sections, OnenoteSectionsApiClient); diff --git a/src/users/onenote_notebooks/request.rs b/src/users/onenote_notebooks/request.rs index 30e0c038..bcb573ea 100644 --- a/src/users/onenote_notebooks/request.rs +++ b/src/users/onenote_notebooks/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( OnenoteNotebooksApiClient, OnenoteNotebooksIdApiClient, ResourceIdentity::OnenoteNotebooks diff --git a/src/users/onenote_pages/request.rs b/src/users/onenote_pages/request.rs index 4d4f2744..cc235ea0 100644 --- a/src/users/onenote_pages/request.rs +++ b/src/users/onenote_pages/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( OnenotePagesApiClient, OnenotePagesIdApiClient, ResourceIdentity::OnenotePages diff --git a/src/users/onenote_section_groups/request.rs b/src/users/onenote_section_groups/request.rs index f0fd9b7d..106bc714 100644 --- a/src/users/onenote_section_groups/request.rs +++ b/src/users/onenote_section_groups/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( OnenoteSectionGroupsApiClient, OnenoteSectionGroupsIdApiClient, ResourceIdentity::OnenoteSectionGroups diff --git a/src/users/onenote_sections/request.rs b/src/users/onenote_sections/request.rs index 0e33fc96..8a94b04e 100644 --- a/src/users/onenote_sections/request.rs +++ b/src/users/onenote_sections/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( OnenoteSectionsApiClient, OnenoteSectionsIdApiClient, ResourceIdentity::OnenoteSections diff --git a/src/users/online_meetings/request.rs b/src/users/online_meetings/request.rs index 78cb22f8..a2519896 100644 --- a/src/users/online_meetings/request.rs +++ b/src/users/online_meetings/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( OnlineMeetingsApiClient, OnlineMeetingsIdApiClient, ResourceIdentity::OnlineMeetings diff --git a/src/users/outlook/request.rs b/src/users/outlook/request.rs index 8730636a..233dc4a6 100644 --- a/src/users/outlook/request.rs +++ b/src/users/outlook/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(OutlookApiClient, ResourceIdentity::Outlook); +api_client!(OutlookApiClient, ResourceIdentity::Outlook); impl OutlookApiClient { get!( diff --git a/src/users/owned_devices/request.rs b/src/users/owned_devices/request.rs index 4696f8b0..dcefa405 100644 --- a/src/users/owned_devices/request.rs +++ b/src/users/owned_devices/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( OwnedDevicesApiClient, OwnedDevicesIdApiClient, ResourceIdentity::OwnedDevices diff --git a/src/users/owned_objects/request.rs b/src/users/owned_objects/request.rs index d24d07b9..3f5c60fb 100644 --- a/src/users/owned_objects/request.rs +++ b/src/users/owned_objects/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( OwnedObjectsApiClient, OwnedObjectsIdApiClient, ResourceIdentity::OwnedObjects diff --git a/src/users/photos/request.rs b/src/users/photos/request.rs index 04fd5472..8979f4b3 100644 --- a/src/users/photos/request.rs +++ b/src/users/photos/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(PhotosApiClient, PhotosIdApiClient, ResourceIdentity::Photos); +api_client!(PhotosApiClient, PhotosIdApiClient, ResourceIdentity::Photos); impl PhotosApiClient { get!( diff --git a/src/users/presence/request.rs b/src/users/presence/request.rs index f4034bea..61d45b9e 100644 --- a/src/users/presence/request.rs +++ b/src/users/presence/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(PresenceApiClient, ResourceIdentity::Presence); +api_client!(PresenceApiClient, ResourceIdentity::Presence); impl PresenceApiClient { delete!( diff --git a/src/users/registered_devices/request.rs b/src/users/registered_devices/request.rs index 38b17e36..5277edc5 100644 --- a/src/users/registered_devices/request.rs +++ b/src/users/registered_devices/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( RegisteredDevicesApiClient, RegisteredDevicesIdApiClient, ResourceIdentity::RegisteredDevices diff --git a/src/users/request.rs b/src/users/request.rs index ebfdf6fd..1cb4216f 100644 --- a/src/users/request.rs +++ b/src/users/request.rs @@ -8,7 +8,7 @@ use crate::oauth2_permission_grants::*; use crate::planner::*; use crate::users::*; -resource_api_client!(UsersApiClient, UsersIdApiClient, ResourceIdentity::Users); +api_client!(UsersApiClient, UsersIdApiClient, ResourceIdentity::Users); impl UsersApiClient { post!( diff --git a/src/users/scoped_role_member_of/request.rs b/src/users/scoped_role_member_of/request.rs index 0cc8d859..47f2aba7 100644 --- a/src/users/scoped_role_member_of/request.rs +++ b/src/users/scoped_role_member_of/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ScopedRoleMemberOfApiClient, ScopedRoleMemberOfIdApiClient, ResourceIdentity::ScopedRoleMemberOf diff --git a/src/users/settings/request.rs b/src/users/settings/request.rs index 20b6a6c6..a0d1af50 100644 --- a/src/users/settings/request.rs +++ b/src/users/settings/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(SettingsApiClient, ResourceIdentity::Settings); +api_client!(SettingsApiClient, ResourceIdentity::Settings); impl SettingsApiClient { delete!( diff --git a/src/users/teamwork/request.rs b/src/users/teamwork/request.rs index 005cbacd..a06787bf 100644 --- a/src/users/teamwork/request.rs +++ b/src/users/teamwork/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!(TeamworkApiClient, ResourceIdentity::Teamwork); +api_client!(TeamworkApiClient, ResourceIdentity::Teamwork); impl TeamworkApiClient { delete!( diff --git a/src/users/todo/request.rs b/src/users/todo/request.rs index 3153717d..7b7837b5 100644 --- a/src/users/todo/request.rs +++ b/src/users/todo/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!(TodoApiClient, ResourceIdentity::Todo); +api_client!(TodoApiClient, ResourceIdentity::Todo); impl TodoApiClient { api_client_link_id!(list, TodoListsIdApiClient); diff --git a/src/users/todo_lists/request.rs b/src/users/todo_lists/request.rs index 6e8cff75..bc1d2d18 100644 --- a/src/users/todo_lists/request.rs +++ b/src/users/todo_lists/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( TodoListsApiClient, TodoListsIdApiClient, ResourceIdentity::TodoLists diff --git a/src/users/todo_lists_tasks/request.rs b/src/users/todo_lists_tasks/request.rs index 8300fb70..d0228dc2 100644 --- a/src/users/todo_lists_tasks/request.rs +++ b/src/users/todo_lists_tasks/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TodoListsTasksApiClient, TodoListsTasksIdApiClient, ResourceIdentity::TodoListsTasks diff --git a/src/users/transitive_member_of/request.rs b/src/users/transitive_member_of/request.rs index e2696b87..8ef4d26e 100644 --- a/src/users/transitive_member_of/request.rs +++ b/src/users/transitive_member_of/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( TransitiveMemberOfApiClient, TransitiveMemberOfIdApiClient, ResourceIdentity::TransitiveMemberOf diff --git a/src/users/users_attachments/request.rs b/src/users/users_attachments/request.rs index dd04558d..f6459f6d 100644 --- a/src/users/users_attachments/request.rs +++ b/src/users/users_attachments/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( UsersAttachmentsApiClient, UsersAttachmentsIdApiClient, ResourceIdentity::UsersAttachments diff --git a/src/users/users_messages/request.rs b/src/users/users_messages/request.rs index 3c9e4a22..85e9429b 100644 --- a/src/users/users_messages/request.rs +++ b/src/users/users_messages/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::users::*; -resource_api_client!( +api_client!( UsersMessagesApiClient, UsersMessagesIdApiClient, ResourceIdentity::UsersMessages diff --git a/tests/download_error.rs b/tests/download_error.rs index 6aed1a97..ef94c365 100644 --- a/tests/download_error.rs +++ b/tests/download_error.rs @@ -27,7 +27,7 @@ async fn download_config_file_exists() { .await; match result { - Ok(response2) => panic!("Download request should have thrown AsyncDownloadError::FileExists. Instead got successful Response: {response2:#?}"), + Ok(response2) => panic!("Download request should have thrown AsyncDownloadError::FileExists. Instead got successful Response: {:#?}", response2), Err(AsyncDownloadError::FileExists(name)) => { if cfg!(target_os = "windows") { From 2a61a6638feb4d001ac2827ac10eee46e6c8f2ca Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 1 May 2023 03:36:04 -0400 Subject: [PATCH 014/118] Add allowed host validator and security controls for webview --- graph-codegen/src/openapi/mod.rs | 2 +- graph-codegen/src/parser/request.rs | 4 +- graph-http/src/core/body_read.rs | 6 +- graph-http/src/url/graphurl.rs | 4 +- graph-oauth/Cargo.toml | 2 + graph-oauth/src/device_code.rs | 2 +- graph-oauth/src/id_token.rs | 4 +- .../src/identity/allowed_host_validator.rs | 112 ++++++++++++++++++ .../auth_code_authorization_url.rs | 17 ++- .../client_credentials_authorization_url.rs | 6 + .../credentials/public_client_application.rs | 66 ++++++++++- .../resource_owner_password_credential.rs | 4 +- graph-oauth/src/identity/mod.rs | 2 + graph-oauth/src/lib.rs | 92 ++++++-------- graph-oauth/src/web/interactive_web_view.rs | 108 +++++++++++++---- .../src/web/interactive_web_view_options.rs | 14 +++ graph-oauth/src/web/mod.rs | 2 + 17 files changed, 352 insertions(+), 95 deletions(-) diff --git a/graph-codegen/src/openapi/mod.rs b/graph-codegen/src/openapi/mod.rs index 5ae9c55a..0071d994 100644 --- a/graph-codegen/src/openapi/mod.rs +++ b/graph-codegen/src/openapi/mod.rs @@ -139,7 +139,7 @@ pub struct OpenApi { /// declared. The tags that are not declared MAY be organized randomly /// or based on the tools' logic. Each tag name in the list MUST be unique. #[serde(skip_serializing_if = "Option::is_none")] - pub tags: Option<serde_json::Value>, + pub tags: Option<Value>, /// Additional external documentation. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/graph-codegen/src/parser/request.rs b/graph-codegen/src/parser/request.rs index ed8030e7..5561cf6d 100644 --- a/graph-codegen/src/parser/request.rs +++ b/graph-codegen/src/parser/request.rs @@ -284,8 +284,8 @@ impl Hash for RequestMap { } impl IntoIterator for RequestMap { - type IntoIter = std::collections::vec_deque::IntoIter<Self::Item>; type Item = Request; + type IntoIter = std::collections::vec_deque::IntoIter<Self::Item>; fn into_iter(self) -> Self::IntoIter { self.requests.into_iter() @@ -591,8 +591,8 @@ impl RequestSet { } impl IntoIterator for RequestSet { - type IntoIter = std::collections::hash_set::IntoIter<Self::Item>; type Item = RequestMap; + type IntoIter = std::collections::hash_set::IntoIter<Self::Item>; fn into_iter(self) -> Self::IntoIter { self.set.into_iter() diff --git a/graph-http/src/core/body_read.rs b/graph-http/src/core/body_read.rs index 631cef9b..2d573521 100644 --- a/graph-http/src/core/body_read.rs +++ b/graph-http/src/core/body_read.rs @@ -9,7 +9,7 @@ use std::io::{BufReader, Read}; pub struct BodyRead { buf: String, blocking_body: Option<reqwest::blocking::Body>, - async_body: Option<reqwest::Body>, + async_body: Option<Body>, } impl BodyRead { @@ -41,7 +41,7 @@ impl BodyRead { } } -impl From<BodyRead> for reqwest::Body { +impl From<BodyRead> for Body { fn from(upload: BodyRead) -> Self { if let Some(body) = upload.async_body { return body; @@ -108,7 +108,7 @@ impl TryFrom<bytes::Bytes> for BodyRead { } } -impl From<reqwest::Body> for BodyRead { +impl From<Body> for BodyRead { fn from(body: Body) -> Self { BodyRead { buf: Default::default(), diff --git a/graph-http/src/url/graphurl.rs b/graph-http/src/url/graphurl.rs index eeec3ae7..3f75779b 100644 --- a/graph-http/src/url/graphurl.rs +++ b/graph-http/src/url/graphurl.rs @@ -74,8 +74,8 @@ impl GraphUrl { self.url.clone() } - pub fn to_reqwest_url(&self) -> reqwest::Url { - reqwest::Url::parse(self.as_str()).unwrap() + pub fn to_reqwest_url(&self) -> Url { + Url::parse(self.as_str()).unwrap() } pub fn query_pairs_mutable(&mut self) -> Serializer<UrlQuery> { diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index bd33e68a..eb1bb89f 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -29,6 +29,8 @@ url = "2" webbrowser = "0.8.7" wry = "0.28.3" uuid = { version = "1.3.1", features = ["v4"] } +log = "0.4" +pretty_env_logger = "0.4" graph-error = { path = "../graph-error" } diff --git a/graph-oauth/src/device_code.rs b/graph-oauth/src/device_code.rs index 4e8c0853..57e10543 100644 --- a/graph-oauth/src/device_code.rs +++ b/graph-oauth/src/device_code.rs @@ -2,7 +2,7 @@ use serde_json::Value; use std::collections::HashMap; use std::time::Duration; -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DeviceCode { pub device_code: String, pub expires_in: u64, diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index c5d1d39a..717365d3 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -1,12 +1,12 @@ use crate::jwt::{JsonWebToken, JwtParser}; -use serde::de::{Error, Visitor}; +use serde::de::Visitor; use serde::{Deserialize, Deserializer}; use serde_json::Value; use std::borrow::Cow; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{Debug, Formatter}; -use std::io::ErrorKind; + use std::str::FromStr; use url::form_urlencoded; diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index e69de29b..3b4e142b 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -0,0 +1,112 @@ +use url::{Host, Url}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum HostValidator { + Valid, + Invalid, +} + +pub trait AllowedHostValidator<RHS = Self> { + fn validate(&self, valid_hosts: &[Url]) -> HostValidator; +} + +impl AllowedHostValidator for Url { + fn validate(&self, valid_hosts: &[Url]) -> HostValidator { + let hosts: Vec<Host<&str>> = valid_hosts.iter().flat_map(|url| url.host()).collect(); + + if let Some(host) = self.host() { + if hosts.contains(&host) { + return HostValidator::Valid; + } + } + + HostValidator::Invalid + } +} + +impl AllowedHostValidator for String { + fn validate(&self, valid_hosts: &[Url]) -> HostValidator { + if let Ok(url) = Url::parse(self) { + return url.validate(valid_hosts); + } + + HostValidator::Invalid + } +} + +impl AllowedHostValidator for &str { + fn validate(&self, valid_hosts: &[Url]) -> HostValidator { + if let Ok(url) = Url::parse(self) { + return url.validate(valid_hosts); + } + + HostValidator::Invalid + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_valid_hosts() { + let valid_hosts: Vec<String> = vec![ + "graph.microsoft.com", + "graph.microsoft.us", + "dod-graph.microsoft.us", + "graph.microsoft.de", + "microsoftgraph.chinacloudapi.cn", + "canary.graph.microsoft.com", + ] + .iter() + .map(|s| s.to_string()) + .collect(); + + let host_urls: Vec<Url> = valid_hosts + .iter() + .map(|s| format!("https://{s}")) + .flat_map(|s| Url::parse(&s)) + .collect(); + + assert_eq!(6, host_urls.len()); + + for url in host_urls.iter() { + assert_eq!(HostValidator::Valid, url.validate(&host_urls)); + } + } + + #[test] + fn test_invalid_hosts() { + let invalid_hosts = [ + "graph.on.microsoft.com", + "microsoft.com", + "windows.net", + "example.org", + ]; + + let valid_hosts: Vec<Url> = vec![ + "graph.microsoft.com", + "graph.microsoft.us", + "dod-graph.microsoft.us", + "graph.microsoft.de", + "microsoftgraph.chinacloudapi.cn", + "canary.graph.microsoft.com", + ] + .iter() + .map(|s| Url::parse(&format!("https://{s}")).unwrap()) + .collect(); + assert_eq!(6, valid_hosts.len()); + + let host_urls: Vec<Url> = invalid_hosts + .iter() + .map(|s| format!("https://{s}")) + .flat_map(|s| Url::parse(&s)) + .collect(); + + assert_eq!(4, host_urls.len()); + + for url in host_urls.iter() { + assert_eq!(HostValidator::Invalid, url.validate(valid_hosts.as_slice())); + } + } +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index a04e2e13..155da51c 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,8 +1,9 @@ use crate::auth::{OAuth, OAuthCredential}; -use crate::grants::GrantType; + use crate::identity::{Authority, AzureAuthorityHost, Prompt, ResponseMode}; use crate::oauth::form_credential::FormCredential; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; +use crate::web::{InteractiveWebView, InteractiveWebViewOptions}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -340,6 +341,20 @@ impl AuthCodeAuthorizationUrlBuilder { pub fn url(&self) -> AuthorizationResult<Url> { self.authorization_code_authorize_url.url() } + + pub fn interactive_authentication( + &self, + interactive_web_view_options: Option<InteractiveWebViewOptions>, + ) -> anyhow::Result<()> { + let url = self.url()?; + let redirect_url = Url::parse(self.authorization_code_authorize_url.redirect_uri.as_str())?; + InteractiveWebView::interactive_authentication( + &url, + &redirect_url, + interactive_web_view_options.unwrap_or_default(), + )?; + Ok(()) + } } #[cfg(test)] diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 2d7f88d5..7d3de731 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -133,3 +133,9 @@ impl ClientCredentialsAuthorizationUrlBuilder { self.client_credentials_authorization_url.url() } } + +impl Default for ClientCredentialsAuthorizationUrlBuilder { + fn default() -> Self { + ClientCredentialsAuthorizationUrlBuilder::new() + } +} diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index a08484d7..809c10aa 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,8 +1,13 @@ use crate::identity::{ - AuthorizationSerializer, ResourceOwnerPasswordCredential, TokenCredentialOptions, + AuthorizationSerializer, AzureAuthorityHost, ResourceOwnerPasswordCredential, + TokenCredentialOptions, TokenRequest, }; +use async_trait::async_trait; +use graph_error::AuthorizationResult; use reqwest::tls::Version; -use reqwest::ClientBuilder; +use reqwest::{ClientBuilder, Response}; +use std::collections::HashMap; +use url::Url; /// Clients incapable of maintaining the confidentiality of their credentials /// (e.g., clients executing on the device used by the resource owner, such as an @@ -28,6 +33,63 @@ impl PublicClientApplication { } } +impl AuthorizationSerializer for PublicClientApplication { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.credential.uri(azure_authority_host) + } + + fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form() + } +} + +#[async_trait] +impl TokenRequest for PublicClientApplication { + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } + + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.credential.uri(&azure_authority_host)?; + let form = self.credential.form()?; + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + let basic_auth = self.credential.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send()?) + } else { + Ok(http_client.post(uri).form(&form).send()?) + } + } + + async fn get_token_async(&mut self) -> anyhow::Result<Response> { + let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let uri = self.credential.uri(&azure_authority_host)?; + let form = self.credential.form()?; + let basic_auth = self.credential.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(self + .http_client + .post(uri) + // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 + .basic_auth(client_identifier, Some(secret)) + .form(&form) + .send() + .await?) + } else { + Ok(self.http_client.post(uri).form(&form).send().await?) + } + } +} + impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { fn from(value: ResourceOwnerPasswordCredential) -> Self { PublicClientApplication { diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 5c085928..30977735 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -84,13 +84,13 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { .grant_type("password") .extend_scopes(self.scopes.iter()); - return self.serializer.authorization_form(vec![ + self.serializer.authorization_form(vec![ FormCredential::Required(OAuthCredential::ClientId), FormCredential::Required(OAuthCredential::Username), FormCredential::Required(OAuthCredential::Password), FormCredential::Required(OAuthCredential::GrantType), FormCredential::NotRequired(OAuthCredential::Scope), - ]); + ]) } } diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 77eee150..d48fe318 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,8 +1,10 @@ +mod allowed_host_validator; mod authority; mod authorization_serializer; mod credentials; pub(crate) mod form_credential; +pub use allowed_host_validator::*; pub use authority::*; pub use authorization_serializer::*; pub use credentials::*; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 8459e857..a1f6f039 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -6,86 +6,60 @@ //! //! ### Supported Authorization Flows //! -//! #### Microsoft OneDrive and SharePoint -//! -//! - [Token Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#token-flow) -//! - [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) -//! //! #### Microsoft Identity Platform //! //! - [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) //! - [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +//! - [Authorization Code Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) //! - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) //! - [Implicit Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) //! - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) -//! - [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +//! - [Client Credentials - Client Secret](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#first-case-access-token-request-with-a-shared-secret) +//! - [Client Credentials - Client Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) //! - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) //! -//! # Example -//! ``` -//! use graph_oauth::oauth::OAuth; -//! let mut oauth = OAuth::new(); -//! oauth -//! .client_id("<YOUR_CLIENT_ID>") -//! .client_secret("<YOUR_CLIENT_SECRET>") -//! .add_scope("files.read") -//! .add_scope("files.readwrite") -//! .add_scope("files.read.all") -//! .add_scope("files.readwrite.all") -//! .add_scope("offline_access") -//! .redirect_uri("http://localhost:8000/redirect") -//! .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") -//! .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") -//! .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") -//! .response_type("code") -//! .logout_url("https://login.microsoftonline.com/common/oauth2/v2.0/logout") -//! .post_logout_redirect_uri("http://localhost:8000/redirect"); -//! ``` -//! Get the access code for the authorization code grant by sending the user to -//! log in using their browser. -//! ```rust,ignore -//! # use graph_oauth::oauth::OAuth; -//! # let mut oauth = OAuth::new(); -//! let mut request = oauth.build().authorization_code_grant(); -//! let _ = request.browser_authorization().open(); -//! ``` -//! -//! The access code will be appended to the url on redirect. Pass -//! this code to the OAuth instance: -//! ``` -//! # use graph_oauth::oauth::OAuth; -//! # let mut oauth = OAuth::new(); -//! oauth.authorization_code("<ACCESS CODE>"); -//! ``` +//! #### Microsoft OneDrive and SharePoint //! -//! Perform an authorization code grant request for an access token: -//! ```rust,ignore -//! # use graph_oauth::oauth::{AccessToken, OAuth}; -//! # let mut oauth = OAuth::new(); -//! let mut request = oauth.build().authorization_code_grant(); +//! Can only be used with personal Microsoft accounts. Not recommended - use the Microsoft +//! Identity Platform if at all possible. //! -//! let response = request.access_token().send()?; -//! println!("{:#?}", access_token); +//! - [Token Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#token-flow) +//! - [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) //! -//! if response.status().is_success() { -//! let mut access_token: AccessToken = response.json()?; //! -//! let jwt = access_token.jwt(); -//! println!("{jwt:#?}"); +//! # Example +//! ``` +//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! -//! // Store in OAuth to make requests for refresh tokens. -//! oauth.access_token(access_token); -//! } else { -//! // See if Microsoft Graph returned an error in the Response body -//! let result: reqwest::Result<serde_json::Value> = response.json()?; -//! println!("{:#?}", result); +//! pub fn authorization_url(client_id: &str) { +//! let _url = AuthorizationCodeCredential::authorization_url_builder() +//! .with_client_id(client_id) +//! .with_redirect_uri("http://localhost:8000/redirect") +//! .with_scope(vec!["user.read"]) +//! .url() +//! .unwrap(); //! } //! +//! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> ConfidentialClientApplication { +//! let credential = AuthorizationCodeCredential::builder() +//! .with_authorization_code(authorization_code) +//! .with_client_id(client_id) +//! .with_client_secret(client_secret) +//! .with_scope(vec!["user.read"]) +//! .with_redirect_uri("http://localhost:8000/redirect") +//! .build(); +//! +//! ConfidentialClientApplication::from(credential) +//! } //! ``` + #[macro_use] extern crate strum; #[macro_use] extern crate serde; +#[macro_use] +extern crate log; +extern crate pretty_env_logger; mod access_token; mod auth; diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 91663f02..4c431625 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,6 +1,6 @@ -use url::{Host, Url}; -use wry::application::window::Theme; -use wry::webview::WebviewExtWindows; +use url::Url; + +use crate::web::InteractiveWebViewOptions; use wry::{ application::{ event::{Event, StartCause, WindowEvent}, @@ -10,22 +10,62 @@ use wry::{ webview::WebViewBuilder, }; -pub struct InteractiveWebView; +#[derive(Debug, Clone)] +pub enum UserEvents { + CloseWindow, + ReachedRedirectUri(Url), + InvalidNavigationAttempt(Option<Url>), +} -impl InteractiveWebView { - fn is_validate_host(uri_to_validate: &Url, validate_against: &Vec<Url>) -> bool { - let hosts: Vec<Host<&str>> = validate_against.iter().flat_map(|uri| uri.host()).collect(); +struct WebViewValidHosts { + start_uri: Url, + redirect_uri: Url, +} + +impl WebViewValidHosts { + fn new(start_uri: Url, redirect_uri: Url) -> anyhow::Result<WebViewValidHosts> { + if start_uri.host().is_none() || redirect_uri.host().is_none() { + return Err(anyhow::Error::msg( + "authorization url and redirect uri must have valid uri host", + )); + } - if let Some(attempted_host) = uri_to_validate.host() { - hosts.contains(&attempted_host) + Ok(WebViewValidHosts { + start_uri, + redirect_uri, + }) + } + + fn is_valid_uri(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + self.start_uri.host().eq(&Some(host.clone())) + || self.redirect_uri.host().eq(&Some(host)) } else { false } } - pub fn interactive_authentication(uri: &Url, redirect_uri: &Url) -> anyhow::Result<()> { - let event_loop: EventLoop<()> = EventLoop::new(); - let valid_uri_vec = vec![uri.clone(), redirect_uri.clone()]; + fn is_redirect_host(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + self.redirect_uri.host().eq(&Some(host)) + } else { + false + } + } +} + +pub struct InteractiveWebView; + +impl InteractiveWebView { + pub fn interactive_authentication( + uri: &Url, + redirect_uri: &Url, + options: InteractiveWebViewOptions, + ) -> anyhow::Result<()> { + let event_loop: EventLoop<UserEvents> = EventLoop::<UserEvents>::with_user_event(); + let proxy = event_loop.create_proxy(); + + let validator = WebViewValidHosts::new(uri.clone(), redirect_uri.clone())?; let window = WindowBuilder::new() .with_title("Sign In") @@ -34,18 +74,30 @@ impl InteractiveWebView { .with_minimizable(true) .with_maximizable(true) .with_resizable(true) - .with_theme(Some(Theme::Dark)) + .with_theme(options.theme.clone()) .build(&event_loop)?; let webview = WebViewBuilder::new(window)? .with_url(uri.as_ref())? - .with_file_drop_handler(|_, _| { - return true; - }) + // Disables file drop + .with_file_drop_handler(|_, _| true) .with_navigation_handler(move |uri| { if let Ok(url) = Url::parse(uri.as_str()) { - InteractiveWebView::is_validate_host(&url, &valid_uri_vec) + let is_valid_host = validator.is_valid_uri(&url); + let is_redirect = validator.is_redirect_host(&url); + + if is_redirect { + let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); + return true; + } + + if !is_valid_host { + let _ = proxy.send_event(UserEvents::CloseWindow); + } + + is_valid_host } else { + let _ = proxy.send_event(UserEvents::CloseWindow); false } }) @@ -56,11 +108,27 @@ impl InteractiveWebView { match event { Event::NewEvents(StartCause::Init) => println!("Wry has started!"), - Event::WindowEvent { - window_id, + Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { event: WindowEvent::CloseRequested, .. - } => *control_flow = ControlFlow::Exit, + } => { + let _ = webview.clear_all_browsing_data(); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { + dbg!(&uri); + let _ = webview.clear_all_browsing_data(); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::InvalidNavigationAttempt(url_option)) => { + error!("WebView or possible attacker attempted to navigate to invalid host - closing window for security reasons. Possible url attempted: {url_option:#?}"); + let _ = webview.clear_all_browsing_data(); + *control_flow = ControlFlow::Exit; + + if options.panic_on_invalid_uri_navigation_attempt { + panic!("WebView or possible attacker attempted to navigate to invalid host. Possible url attempted: {url_option:#?}") + } + } _ => (), } }); diff --git a/graph-oauth/src/web/interactive_web_view_options.rs b/graph-oauth/src/web/interactive_web_view_options.rs index e69de29b..c7c73474 100644 --- a/graph-oauth/src/web/interactive_web_view_options.rs +++ b/graph-oauth/src/web/interactive_web_view_options.rs @@ -0,0 +1,14 @@ +#[derive(Clone)] +pub struct InteractiveWebViewOptions { + pub panic_on_invalid_uri_navigation_attempt: bool, + pub theme: Option<wry::application::window::Theme>, +} + +impl Default for InteractiveWebViewOptions { + fn default() -> Self { + InteractiveWebViewOptions { + panic_on_invalid_uri_navigation_attempt: true, + theme: None, + } + } +} diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs index 7a8571c9..60ebb761 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/web/mod.rs @@ -1,3 +1,5 @@ mod interactive_web_view; +mod interactive_web_view_options; pub use interactive_web_view::*; +pub use interactive_web_view_options::*; From 19d830c2bb9f1dfb45b49509df76d94027ebc0cb Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 5 May 2023 07:07:09 -0400 Subject: [PATCH 015/118] Add Environment Credentials --- .cargo/config.toml | 2 + Cargo.toml | 7 + examples/oauth/device_code.rs | 3 +- examples/oauth/main.rs | 7 +- examples/request_body_helper.rs | 69 +++++++ graph-error/src/graph_failure.rs | 3 - graph-http/src/client.rs | 26 ++- graph-http/src/core/body_read.rs | 3 +- graph-http/src/lib.rs | 3 +- graph-http/src/traits/body_ext.rs | 33 +++ graph-http/src/traits/mod.rs | 2 + graph-http/src/traits/response_ext.rs | 32 --- graph-oauth/src/device_code.rs | 13 +- .../src/identity/allowed_host_validator.rs | 2 + graph-oauth/src/identity/authority.rs | 10 +- .../src/identity/authorization_serializer.rs | 11 +- .../auth_code_authorization_url.rs | 53 +++-- ...thorization_code_certificate_credential.rs | 2 +- .../authorization_code_credential.rs | 6 +- .../client_certificate_credential.rs | 2 +- .../credentials/client_secret_credential.rs | 19 +- .../credentials/code_flow_credential.rs | 3 +- .../confidential_client_application.rs | 38 +++- .../credentials/device_code_credential.rs | 40 ++-- .../credentials/environment_credential.rs | 193 ++++++++++++++++++ graph-oauth/src/identity/credentials/mod.rs | 2 + .../credentials/public_client_application.rs | 37 +++- .../resource_owner_password_credential.rs | 38 +++- .../src/identity/credentials/token_request.rs | 24 ++- .../src/web/interactive_authenticator.rs | 18 ++ graph-oauth/src/web/interactive_web_view.rs | 2 +- .../src/web/interactive_web_view_options.rs | 5 + graph-oauth/src/web/mod.rs | 2 + .../{register_client.rs => api_client.rs} | 0 .../api_macros/{macros.rs => http_macros.rs} | 0 src/client/api_macros/mod.rs | 4 +- src/client/graph.rs | 12 +- test-tools/Cargo.toml | 2 +- test-tools/src/oauth_request.rs | 65 ++++-- 39 files changed, 644 insertions(+), 149 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 examples/request_body_helper.rs create mode 100644 graph-http/src/traits/body_ext.rs create mode 100644 graph-oauth/src/identity/credentials/environment_credential.rs create mode 100644 graph-oauth/src/web/interactive_authenticator.rs rename src/client/api_macros/{register_client.rs => api_client.rs} (100%) rename src/client/api_macros/{macros.rs => http_macros.rs} (100%) diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..a41675fd --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +GRAPH_RS_SDK = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1aac98ca..cdf87c79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,12 @@ graph-http = { path = "./graph-http", version = "1.1.0", default-features=false graph-error = { path = "./graph-error", version = "0.2.2" } graph-core = { path = "./graph-core", version = "0.4.0" } +# When updating or adding new features to this or dependent crates run +# cargo tree -e features -i graph-rs-sdk +# Use this command verify that the dependency is not +# enabled by default due to another package having it as a dependency +# without default-features=false. + [features] default = ["native-tls"] native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls"] @@ -60,6 +66,7 @@ webbrowser = "0.8.7" anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.4" +from_as = "0.2.0" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index e83c9927..205d1d51 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -86,8 +86,7 @@ async fn poll_for_access_token( // The authorization url for device code must be https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode // where tenant can be common, -#[tokio::main] -async fn main() -> GraphResult<()> { +pub async fn device_code() -> GraphResult<()> { let mut oauth = get_oauth(); let mut handler = oauth.build_async().device_code(); diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 7719a4ec..113ee2f9 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -27,8 +27,9 @@ mod open_id_connect; mod signing_keys; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCredential, ClientSecretCredential, - ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, + AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, + ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, + DeviceAuthorizationCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenRequest, }; #[tokio::main] @@ -44,7 +45,7 @@ async fn main() { client_credentials_admin_consent::start_server_main().await; // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code - code_flow::start_server_main().await; + device_code::device_code(); // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc open_id_connect::start_server_main().await; diff --git a/examples/request_body_helper.rs b/examples/request_body_helper.rs new file mode 100644 index 00000000..05957b51 --- /dev/null +++ b/examples/request_body_helper.rs @@ -0,0 +1,69 @@ +#![allow(dead_code)] + +// The BodyRead object is used to provide easy use of many different types. + +/* +In request methods that require a body you can use the following values: + +- Any thing that implements serde serialize +- Anything implementing Read or AsyncRead using BodyRead::from_reader and + BodyRead::from_async_read +- reqwest::Body (Async requests only) +- reqwest::blocking::Body (Blocking requests only) +- FileConfig which is a helper object for downloading files but can also be used + for uploading files. + + +BodyRead can also take serializable objects, reqwest::Body, and reqwest::blocking::Body but +you do not need to use BodyRead for those. You can pass these objects directly to the +body parameter for those api methods that need one. + */ + +use graph_rs_sdk::http::BodyRead; +use graph_rs_sdk::Graph; +use std::fs::File; + +fn main() {} + +// When using reqwest::Body and reqwest::blocking::Body you should only use +// reqwest::Body if your using async and reqwest::blocking::Body when using +// blocking requests. + +// If you use a reqwest::blocking::Body for an async method the tokio runtime +// will error and exit. + +fn use_reqwest_blocking_body() { + let body = reqwest::blocking::Body::from(String::new()); + + let client = Graph::new("token"); + client + .user("id") + .get_mail_tips(body) + .into_blocking() + .send() + .unwrap(); +} + +async fn use_reqwest_async_body() { + let body = reqwest::Body::from(String::new()); + + let client = Graph::new("token"); + client + .user("id") + .get_mail_tips(body) + .into_blocking() + .send() + .unwrap(); +} + +// Using BodyRead + +// BodyRead is basically + +fn use_body_read(file: File) { + let _ = BodyRead::from_read(file).unwrap(); +} + +async fn use_async_body_read(file: tokio::fs::File) { + let _ = BodyRead::from_async_read(file).await.unwrap(); +} diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index dbffff1a..684f1c67 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -20,9 +20,6 @@ pub enum GraphFailure { #[error("Request error:\n{0:#?}")] ReqwestError(#[from] reqwest::Error), - #[error("Request error:\n{0:#?}")] - ReqwestHeaderToStr(#[from] reqwest::header::ToStrError), - #[error("Serde error:\n{0:#?}")] SerdeError(#[from] serde_json::error::Error), diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 54025d1b..a4580c71 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -137,13 +137,24 @@ impl GraphClientConfiguration { let config = self.clone(); let headers = self.config.headers.clone(); let mut builder = reqwest::ClientBuilder::new() - .default_headers(self.config.headers) .referer(self.config.referer) .connection_verbose(self.config.connection_verbose) .https_only(self.config.https_only) .min_tls_version(self.config.min_tls_version) .redirect(Policy::limited(2)); + if !self.config.headers.contains_key(USER_AGENT) { + let mut headers = self.config.headers.clone(); + let version = std::env::var("GRAPH_RS_SDK").unwrap(); + headers.insert( + USER_AGENT, + HeaderValue::from_str(&format!("graph-rs-sdk/{version}")).unwrap(), + ); + builder = builder.default_headers(self.config.headers); + } else { + builder = builder.default_headers(self.config.headers); + } + if let Some(timeout) = self.config.timeout { builder = builder.timeout(timeout); } @@ -163,13 +174,24 @@ impl GraphClientConfiguration { pub(crate) fn build_blocking(self) -> BlockingClient { let headers = self.config.headers.clone(); let mut builder = reqwest::blocking::ClientBuilder::new() - .default_headers(self.config.headers) .referer(self.config.referer) .connection_verbose(self.config.connection_verbose) .https_only(self.config.https_only) .min_tls_version(self.config.min_tls_version) .redirect(Policy::limited(2)); + if !self.config.headers.contains_key(USER_AGENT) { + let mut headers = self.config.headers.clone(); + let version = std::env::var("GRAPH_RS_SDK").unwrap(); + headers.insert( + USER_AGENT, + HeaderValue::from_str(&format!("graph-rs-sdk/{version}")).unwrap(), + ); + builder = builder.default_headers(self.config.headers); + } else { + builder = builder.default_headers(self.config.headers); + } + if let Some(timeout) = self.config.timeout { builder = builder.timeout(timeout); } diff --git a/graph-http/src/core/body_read.rs b/graph-http/src/core/body_read.rs index 2d573521..0228b0de 100644 --- a/graph-http/src/core/body_read.rs +++ b/graph-http/src/core/body_read.rs @@ -1,5 +1,6 @@ use crate::api_impl::FileConfig; -use crate::internal::{AsyncTryFrom, BodyExt}; +use crate::internal::AsyncTryFrom; +use crate::traits::BodyExt; use async_trait::async_trait; use bytes::{Buf, BytesMut}; use graph_error::{GraphFailure, GraphResult}; diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 4e9c9923..faa707f6 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -37,7 +37,8 @@ pub mod api_impl { pub use crate::request_components::RequestComponents; pub use crate::request_handler::RequestHandler; pub use crate::resource_identifier::{ResourceConfig, ResourceIdentifier}; - pub use crate::traits::{BodyExt, ODataQuery}; + pub use crate::traits::BodyExt; + pub use crate::traits::ODataQuery; pub use crate::upload_session::UploadSession; pub use graph_error::{GraphFailure, GraphResult}; } diff --git a/graph-http/src/traits/body_ext.rs b/graph-http/src/traits/body_ext.rs new file mode 100644 index 00000000..7882af3f --- /dev/null +++ b/graph-http/src/traits/body_ext.rs @@ -0,0 +1,33 @@ +use crate::api_impl::{BodyRead, FileConfig}; +use graph_error::GraphResult; + +pub trait BodyExt<RHS = Self> { + fn into_body(self) -> GraphResult<BodyRead>; +} + +impl<U> BodyExt for &U +where + U: serde::Serialize, +{ + fn into_body(self) -> GraphResult<BodyRead> { + BodyRead::from_serialize(self) + } +} + +impl BodyExt for &FileConfig { + fn into_body(self) -> GraphResult<BodyRead> { + BodyRead::try_from(self) + } +} + +impl BodyExt for reqwest::Body { + fn into_body(self) -> GraphResult<BodyRead> { + Ok(BodyRead::from(self)) + } +} + +impl BodyExt for reqwest::blocking::Body { + fn into_body(self) -> GraphResult<BodyRead> { + Ok(BodyRead::from(self)) + } +} diff --git a/graph-http/src/traits/mod.rs b/graph-http/src/traits/mod.rs index 5f7c161e..02f527e7 100644 --- a/graph-http/src/traits/mod.rs +++ b/graph-http/src/traits/mod.rs @@ -1,5 +1,6 @@ mod async_iterator; mod async_try_from; +mod body_ext; mod byte_range; mod http_ext; mod odata_link; @@ -9,6 +10,7 @@ mod response_ext; pub use async_iterator::*; pub use async_try_from::*; +pub use body_ext::*; pub use byte_range::*; pub use http_ext::*; pub use odata_link::*; diff --git a/graph-http/src/traits/response_ext.rs b/graph-http/src/traits/response_ext.rs index 7a5f0475..adef3234 100644 --- a/graph-http/src/traits/response_ext.rs +++ b/graph-http/src/traits/response_ext.rs @@ -1,4 +1,3 @@ -use crate::core::BodyRead; use crate::internal::{ copy_async, create_dir_async, FileConfig, HttpResponseBuilderExt, RangeIter, UploadSession, }; @@ -712,34 +711,3 @@ impl ResponseExt for reqwest::Response { ErrorType::from_u16(status.as_u16()) } } - -pub trait BodyExt<RHS = Self> { - fn into_body(self) -> GraphResult<BodyRead>; -} - -impl<U> BodyExt for &U -where - U: serde::Serialize, -{ - fn into_body(self) -> GraphResult<BodyRead> { - BodyRead::from_serialize(self) - } -} - -impl BodyExt for &FileConfig { - fn into_body(self) -> GraphResult<BodyRead> { - BodyRead::try_from(self) - } -} - -impl BodyExt for reqwest::Body { - fn into_body(self) -> GraphResult<BodyRead> { - Ok(BodyRead::from(self)) - } -} - -impl BodyExt for reqwest::blocking::Body { - fn into_body(self) -> GraphResult<BodyRead> { - Ok(BodyRead::from(self)) - } -} diff --git a/graph-oauth/src/device_code.rs b/graph-oauth/src/device_code.rs index 57e10543..35d1bf1e 100644 --- a/graph-oauth/src/device_code.rs +++ b/graph-oauth/src/device_code.rs @@ -2,14 +2,25 @@ use serde_json::Value; use std::collections::HashMap; use std::time::Duration; +/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DeviceCode { pub device_code: String, pub expires_in: u64, - pub interval: Duration, + /// OPTIONAL + /// The minimum amount of time in seconds that the client + /// SHOULD wait between polling requests to the token endpoint. If no + /// value is provided, clients MUST use 5 as the default. + #[serde(default = "default_interval")] + pub interval: Option<Duration>, pub message: String, pub user_code: String, pub verification_uri: String, + pub verification_uri_complete: Option<String>, #[serde(flatten)] pub additional_fields: HashMap<String, Value>, } + +fn default_interval() -> Option<Duration> { + Some(Duration::from_secs(5)) +} diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index 3b4e142b..9ab2da67 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -12,7 +12,9 @@ pub trait AllowedHostValidator<RHS = Self> { impl AllowedHostValidator for Url { fn validate(&self, valid_hosts: &[Url]) -> HostValidator { + let size_before = valid_hosts.len(); let hosts: Vec<Host<&str>> = valid_hosts.iter().flat_map(|url| url.host()).collect(); + assert_eq!(size_before, hosts.len()); if let Some(host) = self.host() { if hosts.contains(&host) { diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 226fedbb..79d96875 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -3,10 +3,10 @@ use url::Url; /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). /// Authentication libraries from Microsoft (this is not one) call this the /// AzureCloudInstance enum or the Instance url string. -#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AzureAuthorityHost { - /// Custom Value communicating that the AzureCloudInstance. - Custom(String), + // Custom Value communicating that the AzureCloudInstance. + //Custom(String), /// Microsoft Azure public cloud. Maps to https://login.microsoftonline.com #[default] AzurePublic, @@ -23,7 +23,7 @@ pub enum AzureAuthorityHost { impl AsRef<str> for AzureAuthorityHost { fn as_ref(&self) -> &str { match self { - AzureAuthorityHost::Custom(url) => url.as_str(), + //AzureAuthorityHost::Custom(url) => url.as_str(), AzureAuthorityHost::AzurePublic => "https://login.microsoftonline.com", AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn", AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de", @@ -50,7 +50,7 @@ impl AzureAuthorityHost { pub fn default_managed_identity_scope(&self) -> &'static str { match self { - AzureAuthorityHost::Custom(_) => "https://management.azure.com//.default", + //AzureAuthorityHost::Custom(_) => "https://management.azure.com//.default", AzureAuthorityHost::AzurePublic => "https://management.azure.com//.default", AzureAuthorityHost::AzureChina => "https://management.chinacloudapi.cn/.default", AzureAuthorityHost::AzureGermany => "https://management.microsoftazure.de/.default", diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index 4f109c58..2de3849c 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -5,8 +5,17 @@ use url::Url; pub trait AuthorizationSerializer { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>>; + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn basic_auth(&self) -> Option<(String, String)> { None } } + +pub trait AuthorizationUrl { + fn redirect_uri(&self) -> AuthorizationResult<Url>; + fn authorization_url(&self) -> AuthorizationResult<Url>; + fn authorization_url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url>; +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 155da51c..b9d39110 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,9 +1,9 @@ use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::{Authority, AzureAuthorityHost, Prompt, ResponseMode}; +use crate::identity::{Authority, AuthorizationUrl, AzureAuthorityHost, Prompt, ResponseMode}; use crate::oauth::form_credential::FormCredential; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; -use crate::web::{InteractiveWebView, InteractiveWebViewOptions}; +use crate::web::InteractiveAuthenticator; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -62,11 +62,27 @@ impl AuthCodeAuthorizationUrl { AuthCodeAuthorizationUrlBuilder::new() } - pub fn url(&self) -> AuthorizationResult<Url> { + fn url(&self) -> AuthorizationResult<Url> { self.url_with_host(&AzureAuthorityHost::default()) } - pub fn url_with_host( + fn url_with_host(&self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.authorization_url_with_host(azure_authority_host) + } +} + +impl InteractiveAuthenticator for AuthCodeAuthorizationUrl {} + +impl AuthorizationUrl for AuthCodeAuthorizationUrl { + fn redirect_uri(&self) -> AuthorizationResult<Url> { + Url::parse(self.redirect_uri.as_str()).map_err(AuthorizationFailure::from) + } + + fn authorization_url(&self) -> AuthorizationResult<Url> { + self.authorization_url_with_host(&AzureAuthorityHost::default()) + } + + fn authorization_url_with_host( &self, azure_authority_host: &AzureAuthorityHost, ) -> AuthorizationResult<Url> { @@ -325,6 +341,9 @@ impl AuthCodeAuthorizationUrlBuilder { self } + /// Sets the code_challenge and code_challenge_method using the [ProofKeyForCodeExchange] + /// Callers should keep the [ProofKeyForCodeExchange] and provide it to the credential + /// builder in order to set the client verifier and request an access token. pub fn with_proof_key_for_code_exchange( &mut self, proof_key_for_code_exchange: &ProofKeyForCodeExchange, @@ -341,20 +360,6 @@ impl AuthCodeAuthorizationUrlBuilder { pub fn url(&self) -> AuthorizationResult<Url> { self.authorization_code_authorize_url.url() } - - pub fn interactive_authentication( - &self, - interactive_web_view_options: Option<InteractiveWebViewOptions>, - ) -> anyhow::Result<()> { - let url = self.url()?; - let redirect_url = Url::parse(self.authorization_code_authorize_url.redirect_uri.as_str())?; - InteractiveWebView::interactive_authentication( - &url, - &redirect_url, - interactive_web_view_options.unwrap_or_default(), - )?; - Ok(()) - } } #[cfg(test)] @@ -372,4 +377,16 @@ mod test { let url_result = authorizer.url(); assert!(url_result.is_ok()); } + + #[test] + fn url_with_host() { + let authorizer = AuthCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .build(); + + let url_result = authorizer.url_with_host(&AzureAuthorityHost::AzureGermany); + assert!(url_result.is_ok()); + } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 9c1f6f72..01de398c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -108,7 +108,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { return AuthorizationFailure::required_value_msg_result( &format!( diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 9c7fdf8e..4b765d11 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -125,7 +125,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { return AuthorizationFailure::required_value_msg_result( &format!( @@ -361,7 +361,7 @@ mod test { .with_scope(vec!["scope"]) .with_tenant("tenant_id"); let mut credential = credential_builder.build(); - let _ = credential.form().unwrap(); + let _ = credential.form_urlencode().unwrap(); } #[test] @@ -372,6 +372,6 @@ mod test { .with_authorization_code("code") .with_refresh_token("token"); let mut credential = credential_builder.build(); - let _ = credential.form().unwrap(); + let _ = credential.form_urlencode().unwrap(); } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index ddf16cec..e2406bd4 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -93,7 +93,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { } } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index dbbce948..6f4f9b5c 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -19,7 +19,7 @@ use url::Url; /// without immediate interaction with a user, and is often referred to as daemons or service accounts. /// /// See [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ClientSecretCredential { /// Required. /// The Application (client) ID that the Azure portal - App registrations page assigned @@ -57,6 +57,21 @@ impl ClientSecretCredential { } } + pub fn new_with_tenant<T: AsRef<str>>( + tenant_id: T, + client_id: T, + client_secret: T, + ) -> ClientSecretCredential { + ClientSecretCredential { + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + scopes: vec![], + authority: Authority::TenantId(tenant_id.as_ref().to_owned()), + token_credential_options: Default::default(), + serializer: OAuth::new(), + } + } + pub fn builder() -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new() } @@ -83,7 +98,7 @@ impl AuthorizationSerializer for ClientSecretCredential { Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_result(OAuthCredential::ClientId); } diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs index 6a07acc9..aea51c53 100644 --- a/graph-oauth/src/identity/credentials/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -91,7 +91,7 @@ impl AuthorizationSerializer for CodeFlowCredential { } } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.authorization_code.is_some() && self.refresh_token.is_some() { return AuthorizationFailure::required_value_msg_result( &format!( @@ -189,6 +189,7 @@ impl CodeFlowCredentialBuilder { } pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.refresh_token = None; self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 791aca96..bd583796 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -5,10 +5,12 @@ use crate::identity::{ }; use async_trait::async_trait; use graph_error::{AuthorizationResult, GraphResult}; +use reqwest::header::{HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; use url::Url; +use wry::http::HeaderMap; /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), @@ -38,8 +40,8 @@ impl AuthorizationSerializer for ConfidentialClientApplication { self.credential.uri(azure_authority_host) } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { - self.credential.form() + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form_urlencode() } } @@ -50,42 +52,62 @@ impl TokenRequest for ConfidentialClientApplication { } fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let azure_authority_host = self.token_credential_options.azure_authority_host; let uri = self.credential.uri(&azure_authority_host)?; - let form = self.credential.form()?; + let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); let basic_auth = self.credential.basic_auth(); if let Some((client_identifier, secret)) = basic_auth { Ok(http_client .post(uri) .basic_auth(client_identifier, Some(secret)) + // Reqwest adds these automatically but this is here in case that changes. + .headers(headers) .form(&form) .send()?) } else { - Ok(http_client.post(uri).form(&form).send()?) + Ok(http_client.post(uri).headers(headers).form(&form).send()?) } } async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let azure_authority_host = self.token_credential_options.azure_authority_host; let uri = self.credential.uri(&azure_authority_host)?; - let form = self.credential.form()?; + let form = self.credential.form_urlencode()?; let basic_auth = self.credential.basic_auth(); + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + if let Some((client_identifier, secret)) = basic_auth { Ok(self .http_client .post(uri) // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 .basic_auth(client_identifier, Some(secret)) + .headers(headers) .form(&form) .send() .await?) } else { - Ok(self.http_client.post(uri).form(&form).send().await?) + Ok(self + .http_client + .post(uri) + .headers(headers) + .form(&form) + .send() + .await?) } } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 8243544a..716b33e3 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -16,7 +16,7 @@ static DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_c /// and refresh tokens as needed. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code #[derive(Clone)] -pub struct DeviceCodeCredential { +pub struct DeviceAuthorizationCredential { /// Required when requesting a new access token using a refresh token /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. @@ -42,13 +42,13 @@ pub struct DeviceCodeCredential { serializer: OAuth, } -impl DeviceCodeCredential { +impl DeviceAuthorizationCredential { pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( client_id: T, device_code: T, scope: I, - ) -> DeviceCodeCredential { - DeviceCodeCredential { + ) -> DeviceAuthorizationCredential { + DeviceAuthorizationCredential { refresh_token: None, client_id: client_id.as_ref().to_owned(), device_code: Some(device_code.as_ref().to_owned()), @@ -64,7 +64,7 @@ impl DeviceCodeCredential { } } -impl AuthorizationSerializer for DeviceCodeCredential { +impl AuthorizationSerializer for DeviceAuthorizationCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -89,7 +89,7 @@ impl AuthorizationSerializer for DeviceCodeCredential { } } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.device_code.is_some() && self.refresh_token.is_some() { return AuthorizationFailure::required_value_msg_result( &format!( @@ -152,23 +152,23 @@ impl AuthorizationSerializer for DeviceCodeCredential { AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), + OAuthCredential::DeviceCode.alias(), OAuthCredential::RefreshToken.alias() ), - Some("Either authorization code or refresh token is required"), + Some("Either device code or refresh token is required"), ) } } #[derive(Clone)] pub struct DeviceCodeCredentialBuilder { - credential: DeviceCodeCredential, + credential: DeviceAuthorizationCredential, } impl DeviceCodeCredentialBuilder { fn new() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { - credential: DeviceCodeCredential { + credential: DeviceAuthorizationCredential { refresh_token: None, client_id: String::new(), device_code: None, @@ -220,7 +220,7 @@ impl DeviceCodeCredentialBuilder { self.credential.token_credential_options = token_credential_options; } - pub fn build(&self) -> DeviceCodeCredential { + pub fn build(&self) -> DeviceAuthorizationCredential { self.credential.clone() } } @@ -228,7 +228,7 @@ impl DeviceCodeCredentialBuilder { impl From<&DeviceCode> for DeviceCodeCredentialBuilder { fn from(value: &DeviceCode) -> Self { DeviceCodeCredentialBuilder { - credential: DeviceCodeCredential { + credential: DeviceAuthorizationCredential { refresh_token: None, client_id: String::new(), device_code: Some(value.device_code.clone()), @@ -240,3 +240,19 @@ impl From<&DeviceCode> for DeviceCodeCredentialBuilder { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn no_device_code() { + let mut credential = DeviceAuthorizationCredential::builder() + .with_client_id("CLIENT_ID") + .with_scope(vec!["scope"]) + .build(); + + let _ = credential.form_urlencode().unwrap(); + } +} diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs new file mode 100644 index 00000000..db87a9b7 --- /dev/null +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -0,0 +1,193 @@ +use crate::identity::{AuthorizationSerializer, ClientSecretCredential}; +use crate::oauth::{ + ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, +}; +use graph_error::{AuthorizationResult, GraphFailure}; +use std::env::VarError; + +const AZURE_TENANT_ID: &'static str = "AZURE_TENANT_ID"; +const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; +const AZURE_CLIENT_SECRET: &str = "AZURE_CLIENT_SECRET"; +const AZURE_USERNAME: &str = "AZURE_USERNAME"; +const AZURE_PASSWORD: &str = "AZURE_PASSWORD"; + +pub struct EnvironmentCredential { + pub credential: Box<dyn AuthorizationSerializer + Send>, +} + +impl EnvironmentCredential { + pub fn new() -> Result<EnvironmentCredential, VarError> { + match EnvironmentCredential::compile_time_environment_credential() { + Some(credential) => Ok(credential), + None => EnvironmentCredential::runtime_environment_credential(), + } + } + + fn compile_time_environment_credential() -> Option<EnvironmentCredential> { + fn try_azure_client_secret() -> Option<EnvironmentCredential> { + let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let azure_client_id = option_env!("AZURE_CLIENT_ID")?; + let azure_client_secret = option_env!("AZURE_CLIENT_SECRET")?; + + if let Some(tenant_id) = tenant_id_option { + Some(EnvironmentCredential::from( + ConfidentialClientApplication::new( + ClientSecretCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_client_secret, + ), + Default::default(), + ) + .ok()?, + )) + } else { + Some(EnvironmentCredential::from( + ConfidentialClientApplication::new( + ClientSecretCredential::new(azure_client_id, azure_client_secret), + Default::default(), + ) + .ok()?, + )) + } + } + + fn try_username_password() -> Option<EnvironmentCredential> { + let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let azure_client_id = option_env!("AZURE_CLIENT_ID")?; + let azure_username = option_env!("AZURE_USERNAME")?; + let azure_password = option_env!("AZURE_PASSWORD")?; + + match tenant_id_option { + Some(tenant_id) => Some(EnvironmentCredential::from( + PublicClientApplication::new( + ResourceOwnerPasswordCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .ok()?, + )), + None => Some(EnvironmentCredential::from( + PublicClientApplication::new( + ResourceOwnerPasswordCredential::new( + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .ok()?, + )), + } + } + + match try_azure_client_secret() { + Some(credential) => Some(credential), + None => try_username_password(), + } + } + + fn runtime_environment_credential() -> Result<EnvironmentCredential, VarError> { + fn try_azure_client_secret() -> Result<EnvironmentCredential, VarError> { + let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; + let azure_client_secret = std::env::var(AZURE_CLIENT_SECRET)?; + + if let Ok(tenant_id) = tenant_id_result { + Ok(EnvironmentCredential::from( + ConfidentialClientApplication::new( + ClientSecretCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_client_secret, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?, + )) + } else { + Ok(EnvironmentCredential::from( + ConfidentialClientApplication::new( + ClientSecretCredential::new(azure_client_id, azure_client_secret), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?, + )) + } + } + + fn try_username_password() -> Result<EnvironmentCredential, VarError> { + let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; + let azure_username = std::env::var(AZURE_USERNAME)?; + let azure_password = std::env::var(AZURE_PASSWORD)?; + + match tenant_id_result { + Ok(tenant_id) => Ok(EnvironmentCredential::from( + PublicClientApplication::new( + ResourceOwnerPasswordCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?, + )), + Err(_) => Ok(EnvironmentCredential::from( + PublicClientApplication::new( + ResourceOwnerPasswordCredential::new( + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?, + )), + } + } + + match try_azure_client_secret() { + Ok(credential) => Ok(credential), + Err(_) => try_username_password(), + } + } +} + +impl From<ClientSecretCredential> for EnvironmentCredential { + fn from(value: ClientSecretCredential) -> Self { + EnvironmentCredential { + credential: Box::new(value), + } + } +} + +impl From<ResourceOwnerPasswordCredential> for EnvironmentCredential { + fn from(value: ResourceOwnerPasswordCredential) -> Self { + EnvironmentCredential { + credential: Box::new(value), + } + } +} + +impl From<ConfidentialClientApplication> for EnvironmentCredential { + fn from(value: ConfidentialClientApplication) -> Self { + EnvironmentCredential { + credential: Box::new(value), + } + } +} + +impl From<PublicClientApplication> for EnvironmentCredential { + fn from(value: PublicClientApplication) -> Self { + EnvironmentCredential { + credential: Box::new(value), + } + } +} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index cead404b..65a79161 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -8,6 +8,7 @@ mod code_flow_authorization_url; mod code_flow_credential; mod confidential_client_application; mod device_code_credential; +mod environment_credential; mod implicit_credential_authorization_url; mod prompt; mod proof_key_for_code_exchange; @@ -32,6 +33,7 @@ pub use code_flow_authorization_url::*; pub use code_flow_credential::*; pub use confidential_client_application::*; pub use device_code_credential::*; +pub use environment_credential::*; pub use implicit_credential_authorization_url::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 809c10aa..91c45852 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -4,6 +4,7 @@ use crate::identity::{ }; use async_trait::async_trait; use graph_error::AuthorizationResult; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; @@ -13,6 +14,7 @@ use url::Url; /// (e.g., clients executing on the device used by the resource owner, such as an /// installed native application or a web browser-based application), and incapable of /// secure client authentication via any other means. +/// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 pub struct PublicClientApplication { http_client: reqwest::Client, token_credential_options: TokenCredentialOptions, @@ -38,8 +40,8 @@ impl AuthorizationSerializer for PublicClientApplication { self.credential.uri(azure_authority_host) } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { - self.credential.form() + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form_urlencode() } } @@ -50,42 +52,61 @@ impl TokenRequest for PublicClientApplication { } fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let azure_authority_host = self.token_credential_options.azure_authority_host; let uri = self.credential.uri(&azure_authority_host)?; - let form = self.credential.form()?; + let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); let basic_auth = self.credential.basic_auth(); if let Some((client_identifier, secret)) = basic_auth { Ok(http_client .post(uri) .basic_auth(client_identifier, Some(secret)) + .headers(headers) .form(&form) .send()?) } else { - Ok(http_client.post(uri).form(&form).send()?) + Ok(http_client.post(uri).headers(headers).form(&form).send()?) } } async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host.clone(); + let azure_authority_host = self.token_credential_options.azure_authority_host; let uri = self.credential.uri(&azure_authority_host)?; - let form = self.credential.form()?; + let form = self.credential.form_urlencode()?; let basic_auth = self.credential.basic_auth(); + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + if let Some((client_identifier, secret)) = basic_auth { Ok(self .http_client .post(uri) // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 .basic_auth(client_identifier, Some(secret)) + .headers(headers) .form(&form) .send() .await?) } else { - Ok(self.http_client.post(uri).form(&form).send().await?) + Ok(self + .http_client + .post(uri) + .headers(headers) + .form(&form) + .send() + .await?) } } } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 30977735..3ef939b3 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -28,7 +28,7 @@ pub struct ResourceOwnerPasswordCredential { /// identifier (application ID URI) of the resource you want, affixed with the .default /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuth, @@ -36,6 +36,22 @@ pub struct ResourceOwnerPasswordCredential { impl ResourceOwnerPasswordCredential { pub fn new<T: AsRef<str>>( + client_id: T, + username: T, + password: T, + ) -> ResourceOwnerPasswordCredential { + ResourceOwnerPasswordCredential { + client_id: client_id.as_ref().to_owned(), + username: username.as_ref().to_owned(), + password: password.as_ref().to_owned(), + scope: vec![], + authority: Default::default(), + token_credential_options: Default::default(), + serializer: Default::default(), + } + } + + pub fn new_with_tenant<T: AsRef<str>>( tenant: T, client_id: T, username: T, @@ -45,7 +61,7 @@ impl ResourceOwnerPasswordCredential { client_id: client_id.as_ref().to_owned(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - scopes: vec![], + scope: vec![], authority: Authority::TenantId(tenant.as_ref().to_owned()), token_credential_options: Default::default(), serializer: Default::default(), @@ -64,7 +80,7 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } - fn form(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); } @@ -79,19 +95,19 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { self.serializer .client_id(self.client_id.as_str()) - .username(self.username.as_str()) - .password(self.password.as_str()) .grant_type("password") - .extend_scopes(self.scopes.iter()); + .extend_scopes(self.scope.iter()); self.serializer.authorization_form(vec![ FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::Username), - FormCredential::Required(OAuthCredential::Password), FormCredential::Required(OAuthCredential::GrantType), FormCredential::NotRequired(OAuthCredential::Scope), ]) } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.username.to_string(), self.password.to_string())) + } } #[derive(Clone)] @@ -106,7 +122,7 @@ impl ResourceOwnerPasswordCredentialBuilder { client_id: String::new(), username: String::new(), password: String::new(), - scopes: vec![], + scope: vec![], authority: Authority::Organizations, token_credential_options: Default::default(), serializer: Default::default(), @@ -160,8 +176,8 @@ impl ResourceOwnerPasswordCredentialBuilder { } /// Defaults to "https://graph.microsoft.com/.default" - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 0c7d7f97..a2c719a1 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,5 +1,6 @@ use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; @@ -10,11 +11,16 @@ pub trait TokenRequest: AuthorizationSerializer { fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { let options = self.token_credential_options().clone(); let uri = self.uri(&options.azure_authority_host)?; - let form = self.form()?; + let form = self.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 let basic_auth = self.basic_auth(); @@ -22,6 +28,7 @@ pub trait TokenRequest: AuthorizationSerializer { Ok(http_client .post(uri) .basic_auth(client_identifier, Some(secret)) + .headers(headers) .form(&form) .send()?) } else { @@ -32,11 +39,16 @@ pub trait TokenRequest: AuthorizationSerializer { async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { let options = self.token_credential_options().clone(); let uri = self.uri(&options.azure_authority_host)?; - let form = self.form()?; + let form = self.form_urlencode()?; let http_client = ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 let basic_auth = self.basic_auth(); @@ -44,11 +56,17 @@ pub trait TokenRequest: AuthorizationSerializer { Ok(http_client .post(uri) .basic_auth(client_identifier, Some(secret)) + .headers(headers) .form(&form) .send() .await?) } else { - Ok(http_client.post(uri).form(&form).send().await?) + Ok(http_client + .post(uri) + .headers(headers) + .form(&form) + .send() + .await?) } } } diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs new file mode 100644 index 00000000..abaf41c7 --- /dev/null +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -0,0 +1,18 @@ +use crate::identity::AuthorizationUrl; +use crate::web::{InteractiveWebView, InteractiveWebViewOptions}; + +pub trait InteractiveAuthenticator: AuthorizationUrl { + fn interactive_authentication( + &self, + interactive_web_view_options: Option<InteractiveWebViewOptions>, + ) -> anyhow::Result<()> { + let url = self.authorization_url()?; + let redirect_url = self.redirect_uri()?; + InteractiveWebView::interactive_authentication( + &url, + &redirect_url, + interactive_web_view_options.unwrap_or_default(), + )?; + Ok(()) + } +} diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 4c431625..347b05ce 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -74,7 +74,7 @@ impl InteractiveWebView { .with_minimizable(true) .with_maximizable(true) .with_resizable(true) - .with_theme(options.theme.clone()) + .with_theme(options.theme) .build(&event_loop)?; let webview = WebViewBuilder::new(window)? diff --git a/graph-oauth/src/web/interactive_web_view_options.rs b/graph-oauth/src/web/interactive_web_view_options.rs index c7c73474..261fa79d 100644 --- a/graph-oauth/src/web/interactive_web_view_options.rs +++ b/graph-oauth/src/web/interactive_web_view_options.rs @@ -2,6 +2,10 @@ pub struct InteractiveWebViewOptions { pub panic_on_invalid_uri_navigation_attempt: bool, pub theme: Option<wry::application::window::Theme>, + /// Provide a list of ports to use for interactive authentication. + /// This assumes that you have http://localhost or http://localhost:port + /// for each port registered in your ADF application registration. + pub ports: Vec<usize>, } impl Default for InteractiveWebViewOptions { @@ -9,6 +13,7 @@ impl Default for InteractiveWebViewOptions { InteractiveWebViewOptions { panic_on_invalid_uri_navigation_attempt: true, theme: None, + ports: vec![], } } } diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs index 60ebb761..2fe21acd 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/web/mod.rs @@ -1,5 +1,7 @@ +mod interactive_authenticator; mod interactive_web_view; mod interactive_web_view_options; +pub use interactive_authenticator::*; pub use interactive_web_view::*; pub use interactive_web_view_options::*; diff --git a/src/client/api_macros/register_client.rs b/src/client/api_macros/api_client.rs similarity index 100% rename from src/client/api_macros/register_client.rs rename to src/client/api_macros/api_client.rs diff --git a/src/client/api_macros/macros.rs b/src/client/api_macros/http_macros.rs similarity index 100% rename from src/client/api_macros/macros.rs rename to src/client/api_macros/http_macros.rs diff --git a/src/client/api_macros/mod.rs b/src/client/api_macros/mod.rs index 62c51389..50000d27 100644 --- a/src/client/api_macros/mod.rs +++ b/src/client/api_macros/mod.rs @@ -1,6 +1,6 @@ #[macro_use] -pub mod macros; +pub mod http_macros; #[macro_use] -pub mod register_client; +pub mod api_client; #[macro_use] pub mod api_client_link; diff --git a/src/client/graph.rs b/src/client/graph.rs index 7e86e080..ddaaf0ed 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -89,7 +89,7 @@ lazy_static! { #[derive(Debug, Clone)] pub struct Graph { client: Client, - endpoint: Url, + endpoint: url::Url, } impl Graph { @@ -231,7 +231,8 @@ impl Graph { ); } - self.endpoint = url; + self.endpoint.set_host(url.host_str()).unwrap(); + self.endpoint.set_path("v1.0"); } HostValidator::Invalid => panic!("Invalid host"), } @@ -285,7 +286,8 @@ impl Graph { ); } - self.endpoint = url; + self.endpoint.set_host(url.host_str()).unwrap(); + self.endpoint.set_path("v1.0"); } HostValidator::Invalid => panic!("Invalid host"), } @@ -634,7 +636,9 @@ mod test { let mut client = Graph::new("token"); for url in VALID_HOSTS.iter() { client.custom_endpoint(url.as_str()); - assert_eq!(client.url().host_str(), url.host_str()); + let mut url1 = url.clone(); + url1.set_path("v1.0"); + assert_eq!(client.url().clone(), url1); } } diff --git a/test-tools/Cargo.toml b/test-tools/Cargo.toml index 4589e273..e90038de 100644 --- a/test-tools/Cargo.toml +++ b/test-tools/Cargo.toml @@ -10,7 +10,7 @@ publish = false [dependencies] futures = "0.3" -from_as = "0.1" +from_as = "0.2.0" lazy_static = "1.4.0" rand = "0.8" serde = {version = "1", features = ["derive"] } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index cfcdf40c..ca884fbe 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -1,6 +1,8 @@ +#![allow(dead_code)] + use from_as::*; use graph_core::resource::ResourceIdentity; -use graph_rs_sdk::oauth::{AccessToken, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, ClientSecretCredential, OAuth}; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -146,12 +148,17 @@ impl OAuthTestCredentials { ); oauth } + + fn client_credentials(self) -> ClientSecretCredential { + ClientSecretCredential::new(self.client_id.as_str(), self.client_secret.as_str()) + } } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash, AsFile, FromFile)] pub enum OAuthTestClient { ClientCredentials, - ROPC, + ResourceOwnerPasswordCredentials, + AuthorizationCodeCredential, } impl OAuthTestClient { @@ -161,7 +168,12 @@ impl OAuthTestClient { let mut req = { match self { OAuthTestClient::ClientCredentials => oauth.build().client_credentials(), - OAuthTestClient::ROPC => oauth.build().resource_owner_password_credentials(), + OAuthTestClient::ResourceOwnerPasswordCredentials => { + oauth.build().resource_owner_password_credentials() + } + OAuthTestClient::AuthorizationCodeCredential => { + oauth.build().authorization_code_grant() + } } }; @@ -182,7 +194,12 @@ impl OAuthTestClient { let mut req = { match self { OAuthTestClient::ClientCredentials => oauth.build_async().client_credentials(), - OAuthTestClient::ROPC => oauth.build_async().resource_owner_password_credentials(), + OAuthTestClient::ResourceOwnerPasswordCredentials => { + oauth.build_async().resource_owner_password_credentials() + } + OAuthTestClient::AuthorizationCodeCredential => { + oauth.build_async().authorization_code_grant() + } } }; @@ -239,8 +256,8 @@ impl OAuthTestClient { } pub fn graph_by_rid(resource_identity: ResourceIdentity) -> Option<(String, Graph)> { - let mut app_registration = OAuthTestClient::get_app_registration()?; - let client = app_registration.get_by(resource_identity)?; + let app_registration = OAuthTestClient::get_app_registration()?; + let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token(credentials) { @@ -253,8 +270,8 @@ impl OAuthTestClient { pub async fn graph_by_rid_async( resource_identity: ResourceIdentity, ) -> Option<(String, Graph)> { - let mut app_registration = OAuthTestClient::get_app_registration()?; - let client = app_registration.get_by(resource_identity)?; + let app_registration = OAuthTestClient::get_app_registration()?; + let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token_async(credentials).await { Some((id, Graph::new(token.bearer_token()))) @@ -280,8 +297,8 @@ impl OAuthTestClient { } pub fn token(resource_identity: ResourceIdentity) -> Option<AccessToken> { - let mut app_registration = OAuthTestClient::get_app_registration()?; - let client = app_registration.get_by(resource_identity)?; + let app_registration = OAuthTestClient::get_app_registration()?; + let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, _credentials) = client.default_client()?; if let Some((_id, token)) = test_client.request_access_token() { @@ -315,8 +332,13 @@ impl OAuthTestClientMap { pub fn get_any(&self) -> Option<(OAuthTestClient, OAuthTestCredentials)> { let client = self.get(&OAuthTestClient::ClientCredentials); if client.is_none() { - self.get(&OAuthTestClient::ROPC) - .map(|credentials| (OAuthTestClient::ROPC, credentials)) + self.get(&OAuthTestClient::ResourceOwnerPasswordCredentials) + .map(|credentials| { + ( + OAuthTestClient::ResourceOwnerPasswordCredentials, + credentials, + ) + }) } else { client.map(|credentials| (OAuthTestClient::ClientCredentials, credentials)) } @@ -373,8 +395,9 @@ impl AppRegistrationClient { } } -pub trait GetBy<T, U> { - fn get_by(&mut self, value: T) -> U; +pub trait GetAppRegistration { + fn get_by_resource_identity(&self, value: ResourceIdentity) -> Option<AppRegistrationClient>; + fn get_by_str(&self, value: &str) -> Option<AppRegistrationClient>; } #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, AsFile, FromFile)] @@ -389,17 +412,15 @@ impl AppRegistrationMap { } } -impl GetBy<&str, Option<AppRegistrationClient>> for AppRegistrationMap { - fn get_by(&mut self, value: &str) -> Option<AppRegistrationClient> { - self.apps.get(value).cloned() - } -} - -impl GetBy<ResourceIdentity, Option<AppRegistrationClient>> for AppRegistrationMap { - fn get_by(&mut self, value: ResourceIdentity) -> Option<AppRegistrationClient> { +impl GetAppRegistration for AppRegistrationMap { + fn get_by_resource_identity(&self, value: ResourceIdentity) -> Option<AppRegistrationClient> { self.apps .iter() .find(|(_, reg)| reg.test_resources.contains(&value)) .map(|(_, reg)| reg.clone()) } + + fn get_by_str(&self, value: &str) -> Option<AppRegistrationClient> { + self.apps.get(value).cloned() + } } From 5f5bd566953e63b380888613e48017b31576cd65 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 7 May 2023 13:12:19 -0400 Subject: [PATCH 016/118] Add EnvironmentCredential and nonce generation --- examples/oauth/auth_code_grant_pkce.rs | 2 +- examples/oauth/environment_credential.rs | 27 ++ examples/oauth/main.rs | 1 + examples/oauth_certificate/main.rs | 8 +- graph-oauth/src/auth.rs | 2 + .../src/identity/allowed_host_validator.rs | 146 ++++++++++- .../auth_code_authorization_url.rs | 154 ++++++++++-- .../src/identity/credentials/display.rs | 17 ++ .../credentials/environment_credential.rs | 236 ++++++++---------- .../implicit_credential_authorization_url.rs | 131 ++++++++-- graph-oauth/src/identity/credentials/mod.rs | 4 + .../proof_key_for_code_exchange.rs | 11 + .../public_client_application_builder.rs | 10 + .../src/identity/credentials/response_type.rs | 2 +- src/client/graph.rs | 81 +++--- src/client/mod.rs | 7 +- 16 files changed, 608 insertions(+), 231 deletions(-) create mode 100644 examples/oauth/environment_credential.rs create mode 100644 graph-oauth/src/identity/credentials/display.rs create mode 100644 graph-oauth/src/identity/credentials/public_client_application_builder.rs diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index c6a1b726..9e1e9bee 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -36,7 +36,7 @@ fn authorization_sign_in() { .with_client_id(CLIENT_ID) .with_scope(vec!["user.read"]) .with_redirect_uri("http://localhost:8000/redirect") - .with_proof_key_for_code_exchange(&PKCE) + .with_pkce(&PKCE) .url() .unwrap(); diff --git a/examples/oauth/environment_credential.rs b/examples/oauth/environment_credential.rs new file mode 100644 index 00000000..cbdc3d7a --- /dev/null +++ b/examples/oauth/environment_credential.rs @@ -0,0 +1,27 @@ +use graph_oauth::oauth::EnvironmentCredential; +use std::env::VarError; + +// EnvironmentCredential will first look for compile time environment variables +// and then runtime environment variables. + +// You can create a resource owner password credential or a client secret credential +// depending on the environment variables you set. + +// Resource Owner Password Credential Environment Variables: +// "AZURE_TENANT_ID" (Optional - puts the tenant id in the authorization url) +// "AZURE_CLIENT_ID" (Required) +// "AZURE_USERNAME" (Required) +// "AZURE_PASSWORD" (Required) +pub fn username_password() -> Result<(), VarError> { + let public_client_application = EnvironmentCredential::resource_owner_password_credential()?; + Ok(()) +} + +// Client Secret Credentials Environment Variables: +// "AZURE_TENANT_ID" (Optional - puts the tenant id in the authorization url) +// "AZURE_CLIENT_ID" (Required) +// "AZURE_CLIENT_SECRET" (Required) +pub fn client_secret_credential() -> Result<(), VarError> { + let confidential_client_application = EnvironmentCredential::client_secret_credential()?; + Ok(()) +} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 113ee2f9..f4ce48ad 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -21,6 +21,7 @@ mod auth_code_grant_refresh_token; mod client_credentials; mod client_credentials_admin_consent; mod device_code; +mod environment_credential; mod implicit_grant; mod is_access_token_expired; mod open_id_connect; diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index c5609ffe..1de5ce4d 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,7 +4,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCertificateCredential, ClientAssertion, + AccessToken, AuthorizationCodeCertificateCredential, ClientAssertion, ConfidentialClientApplication, PKey, TokenRequest, X509, }; use std::fs::File; @@ -57,14 +57,14 @@ pub struct AccessCode { } pub fn authorization_sign_in(client_id: &str, tenant_id: &str) { - let auth_url_builder = AuthCodeAuthorizationUrl::builder() + let url = AuthorizationCodeCertificateCredential::authorization_url_builder() .with_client_id(client_id) .with_tenant(tenant_id) .with_redirect_uri("http://localhost:8080") .with_scope(vec!["User.Read"]) - .build(); + .url() + .unwrap(); - let url = auth_url_builder.url().unwrap(); // web browser crate in dev dependencies will open to default browser in the system. webbrowser::open(url.as_str()).unwrap(); } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 33f55419..b5d09a7b 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -481,6 +481,8 @@ impl OAuth { self.insert(OAuthCredential::Nonce, value) } + // rand = "0.8.5" + /// Set the prompt for open id. /// /// # Example diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index 9ab2da67..d3732be1 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; +use std::hash::Hash; use url::{Host, Url}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -6,12 +8,16 @@ pub enum HostValidator { Invalid, } -pub trait AllowedHostValidator<RHS = Self> { - fn validate(&self, valid_hosts: &[Url]) -> HostValidator; +pub trait ValidateHosts<RHS = Self> { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator; } -impl AllowedHostValidator for Url { - fn validate(&self, valid_hosts: &[Url]) -> HostValidator { +impl ValidateHosts for Url { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + if valid_hosts.is_empty() { + return HostValidator::Invalid; + } + let size_before = valid_hosts.len(); let hosts: Vec<Host<&str>> = valid_hosts.iter().flat_map(|url| url.host()).collect(); assert_eq!(size_before, hosts.len()); @@ -22,28 +28,109 @@ impl AllowedHostValidator for Url { } } + for value in valid_hosts.iter() { + if !value.scheme().eq("https") { + return HostValidator::Invalid; + } + } + HostValidator::Invalid } } -impl AllowedHostValidator for String { - fn validate(&self, valid_hosts: &[Url]) -> HostValidator { +impl ValidateHosts for String { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { if let Ok(url) = Url::parse(self) { - return url.validate(valid_hosts); + return url.validate_hosts(valid_hosts); } HostValidator::Invalid } } -impl AllowedHostValidator for &str { - fn validate(&self, valid_hosts: &[Url]) -> HostValidator { +impl ValidateHosts for &str { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { if let Ok(url) = Url::parse(self) { - return url.validate(valid_hosts); + return url.validate_hosts(valid_hosts); + } + + HostValidator::Invalid + } +} + +#[derive(Clone, Debug)] +pub struct AllowedHostValidator { + allowed_hosts: HashSet<Url>, +} + +impl AllowedHostValidator { + pub fn new(allowed_hosts: HashSet<Url>) -> AllowedHostValidator { + for url in allowed_hosts.iter() { + if !url.scheme().eq("https") { + panic!("Requires https scheme"); + } + } + + AllowedHostValidator { allowed_hosts } + } + + pub fn validate_str(&self, url_str: &str) -> HostValidator { + if let Ok(url) = Url::parse(url_str) { + return self.validate_hosts(&[url]); } HostValidator::Invalid } + + pub fn validate_url(&self, url: &Url) -> HostValidator { + self.validate_hosts(&[url.clone()]) + } +} + +impl From<&[Url]> for AllowedHostValidator { + fn from(value: &[Url]) -> Self { + let hash_set = HashSet::from_iter(value.iter().cloned()); + AllowedHostValidator::new(hash_set) + } +} + +impl ValidateHosts for AllowedHostValidator { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + if valid_hosts.is_empty() { + return HostValidator::Invalid; + } + + let urls: Vec<Url> = self.allowed_hosts.iter().cloned().collect(); + for url in valid_hosts.iter() { + if url + .validate_hosts(urls.as_slice()) + .eq(&HostValidator::Invalid) + { + return HostValidator::Invalid; + } + } + + HostValidator::Valid + } +} + +impl Default for AllowedHostValidator { + fn default() -> Self { + let urls: HashSet<Url> = vec![ + "https://graph.microsoft.com", + "https://graph.microsoft.us", + "https://dod-graph.microsoft.us", + "https://graph.microsoft.de", + "https://microsoftgraph.chinacloudapi.cn", + "https://canary.graph.microsoft.com", + ] + .iter() + .flat_map(|url_str| Url::parse(url_str)) + .collect(); + assert_eq!(6, urls.len()); + + AllowedHostValidator::new(urls) + } } #[cfg(test)] @@ -73,7 +160,7 @@ mod test { assert_eq!(6, host_urls.len()); for url in host_urls.iter() { - assert_eq!(HostValidator::Valid, url.validate(&host_urls)); + assert_eq!(HostValidator::Valid, url.validate_hosts(&host_urls)); } } @@ -108,7 +195,42 @@ mod test { assert_eq!(4, host_urls.len()); for url in host_urls.iter() { - assert_eq!(HostValidator::Invalid, url.validate(valid_hosts.as_slice())); + assert_eq!( + HostValidator::Invalid, + url.validate_hosts(valid_hosts.as_slice()) + ); + } + } + + #[test] + fn test_allowed_host_validator() { + let valid_hosts: Vec<String> = vec![ + "graph.microsoft.com", + "graph.microsoft.us", + "dod-graph.microsoft.us", + "graph.microsoft.de", + "microsoftgraph.chinacloudapi.cn", + "canary.graph.microsoft.com", + ] + .iter() + .map(|s| s.to_string()) + .collect(); + + let host_urls: Vec<Url> = valid_hosts + .iter() + .map(|s| format!("https://{s}")) + .flat_map(|s| Url::parse(&s)) + .collect(); + + assert_eq!(6, host_urls.len()); + + let allowed_host_validator = AllowedHostValidator::from(host_urls.as_slice()); + + for url in host_urls.iter() { + assert_eq!( + HostValidator::Valid, + allowed_host_validator.validate_url(url) + ); } } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index b9d39110..82e8b0c4 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,10 +1,12 @@ use crate::auth::{OAuth, OAuthCredential}; - use crate::identity::{Authority, AuthorizationUrl, AzureAuthorityHost, Prompt, ResponseMode}; use crate::oauth::form_credential::FormCredential; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; use crate::web::InteractiveAuthenticator; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use ring::rand::SecureRandom; use url::form_urlencoded::Serializer; use url::Url; @@ -28,6 +30,18 @@ pub struct AuthCodeAuthorizationUrl { pub(crate) redirect_uri: String, pub(crate) authority: Authority, pub(crate) response_type: Vec<ResponseType>, + /// Optional + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - query: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter isn't supported when requesting an + /// ID token by using the implicit flow. + /// - fragment: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - form_post: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. pub(crate) response_mode: Option<ResponseMode>, pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, @@ -62,11 +76,14 @@ impl AuthCodeAuthorizationUrl { AuthCodeAuthorizationUrlBuilder::new() } - fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> AuthorizationResult<Url> { self.url_with_host(&AzureAuthorityHost::default()) } - fn url_with_host(&self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + pub fn url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { self.authorization_url_with_host(azure_authority_host) } } @@ -115,26 +132,33 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { serializer.response_mode(response_mode.as_ref()); } } else { - let response_type_string = response_types.join(" "); - let mut response_type = response_type_string.trim(); + let response_type = response_types.join(" ").trim().to_owned(); if response_type.is_empty() { serializer.response_type("code"); - response_type = "code"; } else { serializer.response_type(response_type); } - if response_type.contains("id_token") { - serializer.response_mode(ResponseMode::Fragment.as_ref()); + // Set response_mode + if self.response_type.contains(&ResponseType::IdToken) { + if let Some(response_mode) = self.response_mode.as_ref() { + // id_token requires fragment or form_post. The Microsoft identity + // platform recommends form_post. Unless you explicitly set + // fragment then form_post is used here. Please file an issue + // if you experience encounter related problems. + if response_mode.eq(&ResponseMode::Query) { + serializer.response_mode(ResponseMode::Fragment.as_ref()); + } else { + serializer.response_mode(response_mode.as_ref()); + } + } else { + serializer.response_mode(ResponseMode::Fragment.as_ref()); + } } else if let Some(response_mode) = self.response_mode.as_ref() { serializer.response_mode(response_mode.as_ref()); } } - if let Some(response_mode) = self.response_mode.as_ref() { - serializer.response_mode(response_mode.as_ref()); - } - if let Some(state) = self.state.as_ref() { serializer.state(state.as_str()); } @@ -151,6 +175,10 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { serializer.login_hint(login_hint.as_str()); } + if let Some(nonce) = self.nonce.as_ref() { + serializer.nonce(nonce); + } + if let Some(code_challenge) = self.code_challenge.as_ref() { serializer.code_challenge(code_challenge.as_str()); } @@ -169,6 +197,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { FormCredential::NotRequired(OAuthCredential::Prompt), FormCredential::NotRequired(OAuthCredential::LoginHint), FormCredential::NotRequired(OAuthCredential::DomainHint), + FormCredential::NotRequired(OAuthCredential::Nonce), FormCredential::NotRequired(OAuthCredential::CodeChallenge), FormCredential::NotRequired(OAuthCredential::CodeChallengeMethod), ]; @@ -278,6 +307,32 @@ impl AuthCodeAuthorizationUrlBuilder { self } + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + /// + /// The nonce is generated in the same way as generating a PKCE. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { + let mut buf = [0; 32]; + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); + + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(base_64_random_string.as_bytes()); + + let nonce = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + self.authorization_code_authorize_url.nonce = Some(nonce); + Ok(self) + } + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { self.authorization_code_authorize_url.state = Some(state.as_ref().to_owned()); self @@ -289,9 +344,13 @@ impl AuthCodeAuthorizationUrlBuilder { self } - /// Automatically adds profile, id_token, and offline_access to the scope parameter. - pub fn with_default_scope(&mut self) -> &mut Self { - self.with_scope(vec!["profile", "id_token", "offline_access"]) + /// Automatically adds profile, email, id_token, and offline_access to the scope parameter. + pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { + self.with_nonce_generated()?; + self.with_response_mode(ResponseMode::FormPost); + self.with_response_type(vec![ResponseType::Code, ResponseType::IdToken]); + self.with_scope(vec!["profile", "email", "id_token", "offline_access"]); + Ok(self) } /// Indicates the type of user interaction that is required. Valid values are login, none, @@ -344,7 +403,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// Sets the code_challenge and code_challenge_method using the [ProofKeyForCodeExchange] /// Callers should keep the [ProofKeyForCodeExchange] and provide it to the credential /// builder in order to set the client verifier and request an access token. - pub fn with_proof_key_for_code_exchange( + pub fn with_pkce( &mut self, proof_key_for_code_exchange: &ProofKeyForCodeExchange, ) -> &mut Self { @@ -389,4 +448,67 @@ mod test { let url_result = authorizer.url_with_host(&AzureAuthorityHost::AzureGermany); assert!(url_result.is_ok()); } + + #[test] + fn response_mode_set() { + let url = AuthCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .with_response_type(ResponseType::IdToken) + .url() + .unwrap(); + + let query = url.query().unwrap(); + assert!(query.contains("response_mode=fragment")); + assert!(query.contains("response_type=id_token")); + } + + #[test] + fn response_mode_not_set() { + let url = AuthCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .url() + .unwrap(); + + let query = url.query().unwrap(); + assert!(!query.contains("response_mode")); + assert!(query.contains("response_type=code")); + } + + #[test] + fn multi_response_type_set() { + let url = AuthCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .with_response_mode(ResponseMode::FormPost) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .url() + .unwrap(); + + let query = url.query().unwrap(); + assert!(query.contains("response_mode=form_post")); + assert!(query.contains("response_type=code+id_token")); + } + + #[test] + fn generate_nonce() { + let url = AuthCodeAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_nonce_generated() + .unwrap() + .url() + .unwrap(); + + let query = url.query().unwrap(); + assert!(query.contains("response_mode=fragment")); + assert!(query.contains("response_type=code+id_token")); + assert!(query.contains("nonce")); + } } diff --git a/graph-oauth/src/identity/credentials/display.rs b/graph-oauth/src/identity/credentials/display.rs new file mode 100644 index 00000000..a0451cce --- /dev/null +++ b/graph-oauth/src/identity/credentials/display.rs @@ -0,0 +1,17 @@ +pub enum Display { + /// The Authorization Server SHOULD display the authentication and consent UI + /// consistent with a full User Agent page view. If the display parameter is not + /// specified, this is the default display mode. + Page, + /// The Authorization Server SHOULD display the authentication and consent UI consistent with + /// a popup User Agent window. The popup User Agent window should be of an appropriate size + /// for a login-focused dialog and should not obscure the entire window that it is popping + /// up over. + Popup, + /// The Authorization Server SHOULD display the authentication and consent UI consistent with + /// a device that leverages a touch interface. + Touch, + /// The Authorization Server SHOULD display the authentication and consent UI consistent with + /// a "feature phone" type display. + Wap, +} diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index db87a9b7..cc5e2545 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -2,10 +2,9 @@ use crate::identity::{AuthorizationSerializer, ClientSecretCredential}; use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, }; -use graph_error::{AuthorizationResult, GraphFailure}; use std::env::VarError; -const AZURE_TENANT_ID: &'static str = "AZURE_TENANT_ID"; +const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; const AZURE_CLIENT_SECRET: &str = "AZURE_CLIENT_SECRET"; const AZURE_USERNAME: &str = "AZURE_USERNAME"; @@ -16,146 +15,123 @@ pub struct EnvironmentCredential { } impl EnvironmentCredential { - pub fn new() -> Result<EnvironmentCredential, VarError> { - match EnvironmentCredential::compile_time_environment_credential() { - Some(credential) => Ok(credential), - None => EnvironmentCredential::runtime_environment_credential(), + pub fn resource_owner_password_credential() -> Result<PublicClientApplication, VarError> { + match EnvironmentCredential::try_username_password_compile_time_env() { + Ok(credential) => Ok(credential), + Err(_) => EnvironmentCredential::try_username_password_runtime_env(), } } - fn compile_time_environment_credential() -> Option<EnvironmentCredential> { - fn try_azure_client_secret() -> Option<EnvironmentCredential> { - let tenant_id_option = option_env!("AZURE_TENANT_ID"); - let azure_client_id = option_env!("AZURE_CLIENT_ID")?; - let azure_client_secret = option_env!("AZURE_CLIENT_SECRET")?; - - if let Some(tenant_id) = tenant_id_option { - Some(EnvironmentCredential::from( - ConfidentialClientApplication::new( - ClientSecretCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_client_secret, - ), - Default::default(), - ) - .ok()?, - )) - } else { - Some(EnvironmentCredential::from( - ConfidentialClientApplication::new( - ClientSecretCredential::new(azure_client_id, azure_client_secret), - Default::default(), - ) - .ok()?, - )) - } - } - - fn try_username_password() -> Option<EnvironmentCredential> { - let tenant_id_option = option_env!("AZURE_TENANT_ID"); - let azure_client_id = option_env!("AZURE_CLIENT_ID")?; - let azure_username = option_env!("AZURE_USERNAME")?; - let azure_password = option_env!("AZURE_PASSWORD")?; - - match tenant_id_option { - Some(tenant_id) => Some(EnvironmentCredential::from( - PublicClientApplication::new( - ResourceOwnerPasswordCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .ok()?, - )), - None => Some(EnvironmentCredential::from( - PublicClientApplication::new( - ResourceOwnerPasswordCredential::new( - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .ok()?, - )), - } + pub fn client_secret_credential() -> Result<ConfidentialClientApplication, VarError> { + match EnvironmentCredential::try_azure_client_secret_compile_time_env() { + Ok(credential) => Ok(credential), + Err(_) => EnvironmentCredential::try_azure_client_secret_runtime_env(), } + } - match try_azure_client_secret() { - Some(credential) => Some(credential), - None => try_username_password(), + fn try_azure_client_secret_compile_time_env() -> Result<ConfidentialClientApplication, VarError> + { + let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; + let azure_client_secret = option_env!("AZURE_CLIENT_SECRET").ok_or(VarError::NotPresent)?; + + match tenant_id_option { + Some(tenant_id) => Ok(ConfidentialClientApplication::new( + ClientSecretCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_client_secret, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), + None => Ok(ConfidentialClientApplication::new( + ClientSecretCredential::new(azure_client_id, azure_client_secret), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), } } - fn runtime_environment_credential() -> Result<EnvironmentCredential, VarError> { - fn try_azure_client_secret() -> Result<EnvironmentCredential, VarError> { - let tenant_id_result = std::env::var(AZURE_TENANT_ID); - let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; - let azure_client_secret = std::env::var(AZURE_CLIENT_SECRET)?; - - if let Ok(tenant_id) = tenant_id_result { - Ok(EnvironmentCredential::from( - ConfidentialClientApplication::new( - ClientSecretCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_client_secret, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?, - )) - } else { - Ok(EnvironmentCredential::from( - ConfidentialClientApplication::new( - ClientSecretCredential::new(azure_client_id, azure_client_secret), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?, - )) - } + fn try_azure_client_secret_runtime_env() -> Result<ConfidentialClientApplication, VarError> { + let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; + let azure_client_secret = std::env::var(AZURE_CLIENT_SECRET)?; + + if let Ok(tenant_id) = tenant_id_result { + Ok(ConfidentialClientApplication::new( + ClientSecretCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_client_secret, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?) + } else { + Ok(ConfidentialClientApplication::new( + ClientSecretCredential::new(azure_client_id, azure_client_secret), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?) } + } - fn try_username_password() -> Result<EnvironmentCredential, VarError> { - let tenant_id_result = std::env::var(AZURE_TENANT_ID); - let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; - let azure_username = std::env::var(AZURE_USERNAME)?; - let azure_password = std::env::var(AZURE_PASSWORD)?; - - match tenant_id_result { - Ok(tenant_id) => Ok(EnvironmentCredential::from( - PublicClientApplication::new( - ResourceOwnerPasswordCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?, - )), - Err(_) => Ok(EnvironmentCredential::from( - PublicClientApplication::new( - ResourceOwnerPasswordCredential::new( - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?, - )), - } + fn try_username_password_compile_time_env() -> Result<PublicClientApplication, VarError> { + let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; + let azure_username = option_env!("AZURE_USERNAME").ok_or(VarError::NotPresent)?; + let azure_password = option_env!("AZURE_PASSWORD").ok_or(VarError::NotPresent)?; + + match tenant_id_option { + Some(tenant_id) => Ok(PublicClientApplication::new( + ResourceOwnerPasswordCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), + None => Ok(PublicClientApplication::new( + ResourceOwnerPasswordCredential::new( + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), } + } - match try_azure_client_secret() { - Ok(credential) => Ok(credential), - Err(_) => try_username_password(), + fn try_username_password_runtime_env() -> Result<PublicClientApplication, VarError> { + let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; + let azure_username = std::env::var(AZURE_USERNAME)?; + let azure_password = std::env::var(AZURE_PASSWORD)?; + + match tenant_id_result { + Ok(tenant_id) => Ok(PublicClientApplication::new( + ResourceOwnerPasswordCredential::new_with_tenant( + tenant_id, + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), + Err(_) => Ok(PublicClientApplication::new( + ResourceOwnerPasswordCredential::new( + azure_client_id, + azure_username, + azure_password, + ), + Default::default(), + ) + .map_err(|_| VarError::NotPresent)?), } } } diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index 61a5d316..62cabfcd 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -2,7 +2,10 @@ use crate::auth::{OAuth, OAuthCredential}; use crate::identity::form_credential::FormCredential; use crate::identity::{Authority, AzureAuthorityHost, ResponseMode}; use crate::oauth::{Prompt, ResponseType}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use ring::rand::SecureRandom; use url::form_urlencoded::Serializer; use url::Url; /// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens) @@ -92,7 +95,7 @@ impl ImplicitCredentialAuthorizationUrl { ) -> ImplicitCredentialAuthorizationUrl { ImplicitCredentialAuthorizationUrl { client_id: client_id.as_ref().to_owned(), - response_type: vec![ResponseType::Code], + response_type: vec![ResponseType::Token], redirect_uri: None, scope: scope.into_iter().map(|s| s.to_string()).collect(), response_mode: ResponseMode::Query, @@ -134,28 +137,49 @@ impl ImplicitCredentialAuthorizationUrl { .authority(azure_authority_host, &self.authority); let response_types: Vec<String> = - self.response_type.iter().map(|rt| rt.to_string()).collect(); + self.response_type.iter().map(|s| s.to_string()).collect(); - let mut response_type = response_types.join(" "); - - if response_type.trim().is_empty() { + if response_types.is_empty() { serializer.response_type("code"); - response_type = "code".to_owned(); + serializer.response_mode(self.response_mode.as_ref()); } else { - serializer.response_type(response_type.as_str()); - } + let response_type = response_types.join(" ").trim().to_owned(); + if response_type.is_empty() { + serializer.response_type("code"); + } else { + serializer.response_type(response_type); + } - if response_type.contains("id_token") { - serializer.response_mode(ResponseMode::Fragment.as_ref()); - } else { - serializer.response_mode(self.response_mode.as_ref()); + // Set response_mode + if self.response_type.contains(&ResponseType::IdToken) { + // id_token requires fragment or form_post. The Microsoft identity + // platform recommends form_post. Unless you explicitly set + // fragment then form_post is used here. Please file an issue + // if you experience encounter related problems. + if self.response_mode.eq(&ResponseMode::Query) { + serializer.response_mode(ResponseMode::Fragment.as_ref()); + } else { + serializer.response_mode(self.response_mode.as_ref()); + } + } else { + serializer.response_mode(self.response_mode.as_ref()); + } } + // https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc if self.scope.is_empty() { - if response_type.contains("id_token") { + if self.response_type.contains(&ResponseType::IdToken) { serializer.add_scope("openid"); } else { - return AuthorizationFailure::required_value_result("scope"); + return AuthorizationFailure::required_value_msg_result( + "scope", + Some( + &format!("{} {}", + "scope must be provided or response_type must be id_token which will add openid to scope:", + "https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc" + ) + ) + ); } } @@ -293,6 +317,32 @@ impl ImplicitCredentialAuthorizationUrlBuilder { self } + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + /// + /// The nonce is generated in the same way as generating a PKCE. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { + let mut buf = [0; 32]; + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); + + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(base_64_random_string.as_bytes()); + + let nonce = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + self.implicit_credential_authorization_url.nonce = nonce; + Ok(self) + } + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { self.implicit_credential_authorization_url.state = Some(state.as_ref().to_owned()); self @@ -334,6 +384,10 @@ impl ImplicitCredentialAuthorizationUrlBuilder { pub fn build(&self) -> ImplicitCredentialAuthorizationUrl { self.implicit_credential_authorization_url.clone() } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.implicit_credential_authorization_url.url() + } } #[cfg(test)] @@ -363,6 +417,24 @@ mod test { let authorizer = ImplicitCredentialAuthorizationUrl::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) + .with_response_mode(ResponseMode::Fragment) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_scope(["User.Read"]) + .with_nonce("678910") + .build(); + + let url_result = authorizer.url(); + assert!(url_result.is_ok()); + let url = url_result.unwrap(); + let url_str = url.as_str(); + assert!(url_str.contains("response_mode=fragment")) + } + + #[test] + fn set_open_id_fragment2() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https::/localhost:8080/myapp") .with_scope(["User.Read"]) .with_nonce("678910") @@ -445,4 +517,35 @@ mod test { let url_str = url.as_str(); assert!(url_str.contains("response_type=id_token+token")) } + + #[test] + #[should_panic] + fn missing_scope_panic() { + let authorizer = ImplicitCredentialAuthorizationUrl::builder() + .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") + .with_response_type(vec![ResponseType::Token]) + .with_redirect_uri("https::/localhost:8080/myapp") + .with_nonce("678910") + .build(); + + let _ = authorizer.url().unwrap(); + } + + #[test] + fn generate_nonce() { + let url = ImplicitCredentialAuthorizationUrl::builder() + .with_redirect_uri("https::/localhost:8080") + .with_client_id("client_id") + .with_scope(["read", "write"]) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_nonce_generated() + .unwrap() + .url() + .unwrap(); + + let query = url.query().unwrap(); + assert!(query.contains("response_mode=fragment")); + assert!(query.contains("response_type=code+id_token")); + assert!(query.contains("nonce")); + } } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 65a79161..22995c31 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -8,11 +8,13 @@ mod code_flow_authorization_url; mod code_flow_credential; mod confidential_client_application; mod device_code_credential; +mod display; mod environment_credential; mod implicit_credential_authorization_url; mod prompt; mod proof_key_for_code_exchange; mod public_client_application; +mod public_client_application_builder; mod resource_owner_password_credential; mod response_mode; mod response_type; @@ -33,11 +35,13 @@ pub use code_flow_authorization_url::*; pub use code_flow_credential::*; pub use confidential_client_application::*; pub use device_code_credential::*; +pub use display::*; pub use environment_credential::*; pub use implicit_credential_authorization_url::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; pub use public_client_application::*; +pub use public_client_application_builder::*; pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; diff --git a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs index 641291d9..3a000ccd 100644 --- a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs +++ b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs @@ -68,3 +68,14 @@ impl ProofKeyForCodeExchange { }) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn pkce_generate() { + let pkce = ProofKeyForCodeExchange::generate().unwrap(); + assert_eq!(pkce.code_challenge.len(), 43); + } +} diff --git a/graph-oauth/src/identity/credentials/public_client_application_builder.rs b/graph-oauth/src/identity/credentials/public_client_application_builder.rs new file mode 100644 index 00000000..39e58d88 --- /dev/null +++ b/graph-oauth/src/identity/credentials/public_client_application_builder.rs @@ -0,0 +1,10 @@ +use crate::identity::{EnvironmentCredential, PublicClientApplication}; +use std::env::VarError; + +pub struct PublicClientApplicationBuilder; + +impl PublicClientApplicationBuilder { + pub fn try_from_environment() -> Result<PublicClientApplication, VarError> { + EnvironmentCredential::resource_owner_password_credential() + } +} diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index 84aeb2f4..8ac947b1 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] pub enum ResponseType { #[default] Code, diff --git a/src/client/graph.rs b/src/client/graph.rs index ddaaf0ed..9c34b7bc 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,6 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; +use crate::oauth::{AccessToken, AllowedHostValidator, HostValidator, OAuth}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -65,8 +66,6 @@ use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_error::GraphFailure; use graph_http::api_impl::GraphClientConfiguration; -use graph_oauth::identity::{AllowedHostValidator, HostValidator}; -use graph_oauth::oauth::{AccessToken, OAuth}; use lazy_static::lazy_static; use std::convert::TryFrom; @@ -74,22 +73,13 @@ lazy_static! { static ref PARSED_GRAPH_URL: Url = Url::parse(GRAPH_URL).expect("Unable to set v1 endpoint"); static ref PARSED_GRAPH_URL_BETA: Url = Url::parse(GRAPH_URL_BETA).expect("Unable to set beta endpoint"); - static ref VALID_HOSTS: Vec<Url> = vec![ - Url::parse("https://graph.microsoft.com").expect("Unable to parse url for valid host"), - Url::parse("https://graph.microsoft.us").expect("Unable to parse url for valid host"), - Url::parse("https://dod-graph.microsoft.us").expect("Unable to parse url for valid host"), - Url::parse("https://graph.microsoft.de").expect("Unable to parse url for valid host"), - Url::parse("https://microsoftgraph.chinacloudapi.cn") - .expect("Unable to parse url for valid host"), - Url::parse("https://canary.graph.microsoft.com") - .expect("Unable to parse url for valid host") - ]; } #[derive(Debug, Clone)] pub struct Graph { client: Client, - endpoint: url::Url, + endpoint: Url, + allowed_host_validator: AllowedHostValidator, } impl Graph { @@ -97,6 +87,7 @@ impl Graph { Graph { client: Client::new(access_token), endpoint: PARSED_GRAPH_URL.clone(), + allowed_host_validator: AllowedHostValidator::default(), } } @@ -181,7 +172,9 @@ impl Graph { &self.endpoint } - /// Set a custom endpoint for the Microsoft Graph API + /// Set a custom endpoint for the Microsoft Graph API. Provide the scheme and host with an + /// optional path. The path is not set by the sdk when using a custom endpoint. + /// /// The scheme must be https:// and any other provided scheme will cause a panic. /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// @@ -214,32 +207,20 @@ impl Graph { /// /// let mut client = Graph::new("ACCESS_TOKEN"); /// - /// client.custom_endpoint("https://graph.microsoft.com") + /// client.custom_endpoint("https://graph.microsoft.com/v1.0") /// .me() /// .get_user() /// .send() /// .await?; /// ``` pub fn custom_endpoint(&mut self, custom_endpoint: &str) -> &mut Graph { - match custom_endpoint.validate(&VALID_HOSTS) { - HostValidator::Valid => { - let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); - - if !url.scheme().eq("https") || !url.path().eq("/") || url.query().is_some() { - panic!( - "Invalid path or query - Provide only the host of the Uri such as https://graph.microsoft.com" - ); - } - - self.endpoint.set_host(url.host_str()).unwrap(); - self.endpoint.set_path("v1.0"); - } - HostValidator::Invalid => panic!("Invalid host"), - } + self.use_endpoint(custom_endpoint); self } - /// Set a custom endpoint for the Microsoft Graph API. Provide the scheme and host. + /// Set a custom endpoint for the Microsoft Graph API. Provide the scheme and host with an + /// optional path. The path is not set by the sdk when using a custom endpoint. + /// /// The scheme must be https:// and any other provided scheme will cause a panic. /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// @@ -271,23 +252,23 @@ impl Graph { /// use graph_rs_sdk::Graph; /// /// let mut client = Graph::new("ACCESS_TOKEN"); - /// client.use_endpoint("https://graph.microsoft.com"); + /// client.use_endpoint("https://graph.microsoft.com/v1.0"); /// - /// assert_eq!(client.url().to_string(), "https://graph.microsoft.com/".to_string()) + /// assert_eq!(client.url().to_string(), "https://graph.microsoft.com/v1.0".to_string()) /// ``` pub fn use_endpoint(&mut self, custom_endpoint: &str) { - match custom_endpoint.validate(&VALID_HOSTS) { + match self.allowed_host_validator.validate_str(custom_endpoint) { HostValidator::Valid => { let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); - if !url.scheme().eq("https") || !url.path().eq("/") || url.query().is_some() { + if url.query().is_some() { panic!( - "Invalid path query - Provide only the host of the Uri such as https://graph.microsoft.com" + "Invalid query - Provide only the scheme, host, and optional path of the Uri such as https://graph.microsoft.com/v1.0" ); } self.endpoint.set_host(url.host_str()).unwrap(); - self.endpoint.set_path("v1.0"); + self.endpoint.set_path(url.path()); } HostValidator::Invalid => panic!("Invalid host"), } @@ -567,6 +548,7 @@ impl From<GraphClientConfiguration> for Graph { Graph { client: graph_client_builder.build(), endpoint: PARSED_GRAPH_URL.clone(), + allowed_host_validator: AllowedHostValidator::default(), } } } @@ -633,21 +615,20 @@ mod test { #[test] fn try_valid_hosts() { - let mut client = Graph::new("token"); - for url in VALID_HOSTS.iter() { - client.custom_endpoint(url.as_str()); - let mut url1 = url.clone(); - url1.set_path("v1.0"); - assert_eq!(client.url().clone(), url1); - } - } + let urls = vec![ + "https://graph.microsoft.com/v1.0", + "https://graph.microsoft.us", + "https://dod-graph.microsoft.us", + "https://graph.microsoft.de", + "https://microsoftgraph.chinacloudapi.cn", + "https://canary.graph.microsoft.com", + ]; - #[test] - fn try_valid_hosts2() { let mut client = Graph::new("token"); - for url in VALID_HOSTS.iter() { - client.use_endpoint(url.as_str()); - assert_eq!(client.url().host_str(), url.host_str()); + + for url in urls.iter() { + client.custom_endpoint(url); + assert_eq!(client.url().clone(), Url::parse(url).unwrap()); } } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 6f3341bb..080aad9e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,7 +1,8 @@ -pub(crate) use common::*; -pub use graph::*; - #[macro_use] pub mod api_macros; pub mod common; + mod graph; + +pub(crate) use common::*; +pub use graph::*; From 243e97c75612c2e4b84e51554f6d724b1b59bdb9 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 10 May 2023 02:44:26 -0400 Subject: [PATCH 017/118] Renaming oauth struct, web view impl, and open id base objects --- examples/oauth/auth_code_grant.rs | 8 +- examples/oauth/device_code.rs | 6 +- examples/oauth/open_id_connect.rs | 6 +- examples/oauth/signing_keys.rs | 8 +- examples/request_body_helper.rs | 10 +- graph-oauth/Cargo.toml | 1 + graph-oauth/src/auth.rs | 794 +++++++++--------- graph-oauth/src/auth_response_query.rs | 23 + graph-oauth/src/discovery/graph_discovery.rs | 10 +- graph-oauth/src/grants.rs | 208 ++--- graph-oauth/src/id_token.rs | 9 +- .../auth_code_authorization_url.rs | 237 ++++-- ...thorization_code_certificate_credential.rs | 56 +- .../authorization_code_credential.rs | 63 +- .../client_certificate_credential.rs | 47 +- .../client_credentials_authorization_url.rs | 20 +- .../credentials/client_secret_credential.rs | 20 +- .../code_flow_authorization_url.rs | 16 +- .../credentials/code_flow_credential.rs | 53 +- .../src/identity/credentials/crypto.rs | 23 + .../credentials/device_code_credential.rs | 47 +- .../implicit_credential_authorization_url.rs | 28 +- graph-oauth/src/identity/credentials/mod.rs | 6 + .../credentials/open_id_authorization_url.rs | 299 +++++++ .../credentials/open_id_credential.rs | 1 + .../src/identity/credentials/prompt.rs | 9 + .../resource_owner_password_credential.rs | 20 +- .../src/identity/credentials/response_type.rs | 2 +- .../token_flow_authorization_url.rs | 16 +- graph-oauth/src/identity/form_credential.rs | 8 +- graph-oauth/src/lib.rs | 6 +- graph-oauth/src/oauth_error.rs | 6 +- .../src/web/interactive_authenticator.rs | 28 +- graph-oauth/src/web/interactive_web_view.rs | 25 +- .../src/web/interactive_web_view_options.rs | 5 + src/client/graph.rs | 6 +- src/lib.rs | 4 +- test-tools/src/oauth.rs | 44 +- test-tools/src/oauth_request.rs | 10 +- tests/discovery_tests.rs | 32 +- tests/grants_authorization_code.rs | 10 +- tests/grants_code_flow.rs | 14 +- tests/grants_implicit.rs | 4 +- tests/grants_openid.rs | 6 +- tests/grants_token_flow.rs | 4 +- tests/oauth_tests.rs | 83 +- 46 files changed, 1420 insertions(+), 921 deletions(-) create mode 100644 graph-oauth/src/auth_response_query.rs create mode 100644 graph-oauth/src/identity/credentials/crypto.rs create mode 100644 graph-oauth/src/identity/credentials/open_id_authorization_url.rs create mode 100644 graph-oauth/src/identity/credentials/open_id_credential.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 373df79b..663d3d53 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -84,15 +84,11 @@ async fn handle_redirect( if response.status().is_success() { let mut access_token: AccessToken = response.json().await.unwrap(); - // Option<&JsonWebToken> - let jwt = access_token.jwt(); - println!("{jwt:#?}"); - + // Enables the printing of the bearer, refresh, and id token. + access_token.enable_pii_logging(true); println!("{:#?}", access_token); // This will print the actual access token to the console. - println!("Access Token: {:#?}", access_token.bearer_token()); - println!("Refresh Token: {:#?}", access_token.refresh_token()); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 205d1d51..b84b5978 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,13 +1,13 @@ -use graph_rs_sdk::oauth::{AccessToken, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; // https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code // Update the client id with your own. -fn get_oauth() -> OAuth { +fn get_oauth() -> OAuthSerializer { let client_id = "CLIENT_ID"; - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id(client_id) diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index eb89d9be..bad0f460 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; /// # Example /// ``` /// use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuth}; @@ -19,8 +19,8 @@ use warp::Filter; static CLIENT_ID: &str = "<YOUR_CLIENT_ID>"; static CLIENT_SECRET: &str = "<YOUR_CLIENT_SECRET>"; -fn oauth_open_id() -> OAuth { - let mut oauth = OAuth::new(); +fn oauth_open_id() -> OAuthSerializer { + let mut oauth = OAuthSerializer::new(); oauth .client_id(CLIENT_ID) .client_secret(CLIENT_SECRET) diff --git a/examples/oauth/signing_keys.rs b/examples/oauth/signing_keys.rs index 013f6372..1716be25 100644 --- a/examples/oauth/signing_keys.rs +++ b/examples/oauth/signing_keys.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::oauth::graph_discovery::{ GraphDiscovery, MicrosoftSigningKeysV1, MicrosoftSigningKeysV2, }; -use graph_rs_sdk::oauth::OAuth; +use graph_rs_sdk::oauth::OAuthSerializer; fn get_signing_keys() { // Lists info such as the authorization and token urls, jwks uri, and response types supported. @@ -16,11 +16,11 @@ fn get_signing_keys() { // configuration time when setting values for OAuth. However, this will disregard // all other parameters for the MicrosoftSigningKeys. Use this if you do not // need the other values. - let _oauth: OAuth = GraphDiscovery::V1.oauth().unwrap(); + let _oauth: OAuthSerializer = GraphDiscovery::V1.oauth().unwrap(); } fn tenant_discovery() { - let _oauth: OAuth = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) + let _oauth: OAuthSerializer = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) .oauth() .unwrap(); } @@ -37,7 +37,7 @@ async fn async_keys_discovery() { } async fn async_tenant_discovery() { - let _oauth: OAuth = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) + let _oauth: OAuthSerializer = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) .async_oauth() .await .unwrap(); diff --git a/examples/request_body_helper.rs b/examples/request_body_helper.rs index 05957b51..2150d590 100644 --- a/examples/request_body_helper.rs +++ b/examples/request_body_helper.rs @@ -48,17 +48,13 @@ async fn use_reqwest_async_body() { let body = reqwest::Body::from(String::new()); let client = Graph::new("token"); - client - .user("id") - .get_mail_tips(body) - .into_blocking() - .send() - .unwrap(); + client.user("id").get_mail_tips(body).send().await.unwrap(); } // Using BodyRead -// BodyRead is basically +// BodyRead is a helper struct for using many different types +// as the body of a request. fn use_body_read(file: File) { let _ = BodyRead::from_read(file).unwrap(); diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index eb1bb89f..4fa021ca 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -24,6 +24,7 @@ ring = "0.16.15" serde = { version = "1", features = ["derive"] } serde-aux = "4.1.2" serde_json = "1" +serde_urlencoded = "0.7.1" strum = { version = "0.24.1", features = ["derive"] } url = "2" webbrowser = "0.8.7" diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index b5d09a7b..cf700850 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,7 +1,7 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; -use crate::identity::form_credential::FormCredential; +use crate::identity::form_credential::SerializerField; use crate::identity::{Authority, AzureAuthorityHost}; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; @@ -20,7 +20,7 @@ use url::Url; #[derive( Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, )] -pub enum OAuthCredential { +pub enum OAuthParameter { ClientId, ClientSecret, AuthorizationUrl, @@ -55,67 +55,67 @@ pub enum OAuthCredential { DeviceCode, } -impl OAuthCredential { +impl OAuthParameter { pub fn alias(self) -> &'static str { match self { - OAuthCredential::ClientId => "client_id", - OAuthCredential::ClientSecret => "client_secret", - OAuthCredential::AuthorizationUrl => "authorization_url", - OAuthCredential::AccessTokenUrl => "access_token_url", - OAuthCredential::RefreshTokenUrl => "refresh_token_url", - OAuthCredential::RedirectUri => "redirect_uri", - OAuthCredential::AuthorizationCode => "code", - OAuthCredential::AccessToken => "access_token", - OAuthCredential::RefreshToken => "refresh_token", - OAuthCredential::ResponseMode => "response_mode", - OAuthCredential::ResponseType => "response_type", - OAuthCredential::State => "state", - OAuthCredential::SessionState => "session_state", - OAuthCredential::GrantType => "grant_type", - OAuthCredential::Nonce => "nonce", - OAuthCredential::Prompt => "prompt", - OAuthCredential::IdToken => "id_token", - OAuthCredential::Resource => "resource", - OAuthCredential::DomainHint => "domain_hint", - OAuthCredential::Scope => "scope", - OAuthCredential::LoginHint => "login_hint", - OAuthCredential::ClientAssertion => "client_assertion", - OAuthCredential::ClientAssertionType => "client_assertion_type", - OAuthCredential::CodeVerifier => "code_verifier", - OAuthCredential::CodeChallenge => "code_challenge", - OAuthCredential::CodeChallengeMethod => "code_challenge_method", - OAuthCredential::LogoutURL => "logout_url", - OAuthCredential::PostLogoutRedirectURI => "post_logout_redirect_uri", - OAuthCredential::AdminConsent => "admin_consent", - OAuthCredential::Username => "username", - OAuthCredential::Password => "password", - OAuthCredential::DeviceCode => "device_code", + OAuthParameter::ClientId => "client_id", + OAuthParameter::ClientSecret => "client_secret", + OAuthParameter::AuthorizationUrl => "authorization_url", + OAuthParameter::AccessTokenUrl => "access_token_url", + OAuthParameter::RefreshTokenUrl => "refresh_token_url", + OAuthParameter::RedirectUri => "redirect_uri", + OAuthParameter::AuthorizationCode => "code", + OAuthParameter::AccessToken => "access_token", + OAuthParameter::RefreshToken => "refresh_token", + OAuthParameter::ResponseMode => "response_mode", + OAuthParameter::ResponseType => "response_type", + OAuthParameter::State => "state", + OAuthParameter::SessionState => "session_state", + OAuthParameter::GrantType => "grant_type", + OAuthParameter::Nonce => "nonce", + OAuthParameter::Prompt => "prompt", + OAuthParameter::IdToken => "id_token", + OAuthParameter::Resource => "resource", + OAuthParameter::DomainHint => "domain_hint", + OAuthParameter::Scope => "scope", + OAuthParameter::LoginHint => "login_hint", + OAuthParameter::ClientAssertion => "client_assertion", + OAuthParameter::ClientAssertionType => "client_assertion_type", + OAuthParameter::CodeVerifier => "code_verifier", + OAuthParameter::CodeChallenge => "code_challenge", + OAuthParameter::CodeChallengeMethod => "code_challenge_method", + OAuthParameter::LogoutURL => "logout_url", + OAuthParameter::PostLogoutRedirectURI => "post_logout_redirect_uri", + OAuthParameter::AdminConsent => "admin_consent", + OAuthParameter::Username => "username", + OAuthParameter::Password => "password", + OAuthParameter::DeviceCode => "device_code", } } fn is_debug_redacted(&self) -> bool { matches!( self, - OAuthCredential::ClientId - | OAuthCredential::ClientSecret - | OAuthCredential::AccessToken - | OAuthCredential::RefreshToken - | OAuthCredential::IdToken - | OAuthCredential::CodeVerifier - | OAuthCredential::CodeChallenge - | OAuthCredential::Password - | OAuthCredential::AuthorizationCode + OAuthParameter::ClientId + | OAuthParameter::ClientSecret + | OAuthParameter::AccessToken + | OAuthParameter::RefreshToken + | OAuthParameter::IdToken + | OAuthParameter::CodeVerifier + | OAuthParameter::CodeChallenge + | OAuthParameter::Password + | OAuthParameter::AuthorizationCode ) } } -impl ToString for OAuthCredential { +impl ToString for OAuthParameter { fn to_string(&self) -> String { self.alias().to_string() } } -impl AsRef<str> for OAuthCredential { +impl AsRef<str> for OAuthParameter { fn as_ref(&self) -> &'static str { self.alias() } @@ -143,55 +143,55 @@ impl AsRef<str> for OAuthCredential { /// /// # Example /// ``` -/// use graph_oauth::oauth::OAuth; -/// let oauth = OAuth::new(); +/// use graph_oauth::oauth::OAuthSerializer; +/// let oauth = OAuthSerializer::new(); /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct OAuth { +pub struct OAuthSerializer { access_token: Option<AccessToken>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, } -impl OAuth { +impl OAuthSerializer { /// Create a new OAuth instance. /// /// # Example /// ``` - /// use graph_oauth::oauth::{OAuth, GrantType}; + /// use graph_oauth::oauth::{OAuthSerializer, GrantType}; /// - /// let mut oauth = OAuth::new(); + /// let mut oauth = OAuthSerializer::new(); /// ``` - pub fn new() -> OAuth { - OAuth { + pub fn new() -> OAuthSerializer { + OAuthSerializer { access_token: None, scopes: BTreeSet::new(), credentials: BTreeMap::new(), } } - /// Insert oauth credentials using the OAuthCredential enum. + /// Insert oauth credentials using the OAuthParameter enum. /// This method is used internally for each of the setter methods. /// Callers can optionally use this method to set credentials instead /// of the individual setter methods. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); - /// oauth.insert(OAuthCredential::AuthorizationUrl, "https://example.com"); - /// assert!(oauth.contains(OAuthCredential::AuthorizationUrl)); - /// println!("{:#?}", oauth.get(OAuthCredential::AuthorizationUrl)); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); + /// oauth.insert(OAuthParameter::AuthorizationUrl, "https://example.com"); + /// assert!(oauth.contains(OAuthParameter::AuthorizationUrl)); + /// println!("{:#?}", oauth.get(OAuthParameter::AuthorizationUrl)); /// ``` - pub fn insert<V: ToString>(&mut self, oac: OAuthCredential, value: V) -> &mut OAuth { + pub fn insert<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut OAuthSerializer { let v = value.to_string(); match oac { - OAuthCredential::RefreshTokenUrl - | OAuthCredential::PostLogoutRedirectURI - | OAuthCredential::AccessTokenUrl - | OAuthCredential::AuthorizationUrl - | OAuthCredential::LogoutURL => { + OAuthParameter::RefreshTokenUrl + | OAuthParameter::PostLogoutRedirectURI + | OAuthParameter::AccessTokenUrl + | OAuthParameter::AuthorizationUrl + | OAuthParameter::LogoutURL => { Url::parse(v.as_ref()).unwrap(); } _ => {} @@ -203,24 +203,24 @@ impl OAuth { /// Insert and OAuth credential using the entry trait and /// returning the credential. This internally calls - /// `entry.(OAuthCredential).or_insret_with(value)`. + /// `entry.(OAuthParameter).or_insret_with(value)`. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); - /// let entry = oauth.entry(OAuthCredential::AuthorizationUrl, "https://example.com"); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); + /// let entry = oauth.entry(OAuthParameter::AuthorizationUrl, "https://example.com"); /// assert_eq!(entry, "https://example.com") /// ``` - pub fn entry<V: ToString>(&mut self, oac: OAuthCredential, value: V) -> &mut String { + pub fn entry<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { let v = value.to_string(); match oac { - OAuthCredential::RefreshTokenUrl - | OAuthCredential::PostLogoutRedirectURI - | OAuthCredential::AccessTokenUrl - | OAuthCredential::AuthorizationUrl - | OAuthCredential::LogoutURL => { + OAuthParameter::RefreshTokenUrl + | OAuthParameter::PostLogoutRedirectURI + | OAuthParameter::AccessTokenUrl + | OAuthParameter::AuthorizationUrl + | OAuthParameter::LogoutURL => { Url::parse(v.as_ref()).unwrap(); } _ => {} @@ -235,12 +235,12 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); - /// let a = oauth.get(OAuthCredential::AuthorizationUrl); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); + /// let a = oauth.get(OAuthParameter::AuthorizationUrl); /// ``` - pub fn get(&self, oac: OAuthCredential) -> Option<String> { + pub fn get(&self, oac: OAuthParameter) -> Option<String> { self.credentials.get(oac.alias()).cloned() } @@ -248,13 +248,13 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); - /// println!("{:#?}", oauth.contains(OAuthCredential::Nonce)); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); + /// println!("{:#?}", oauth.contains(OAuthParameter::Nonce)); /// ``` - pub fn contains(&self, t: OAuthCredential) -> bool { - if t == OAuthCredential::Scope { + pub fn contains(&self, t: OAuthParameter) -> bool { + if t == OAuthParameter::Scope { return !self.scopes.is_empty(); } self.credentials.contains_key(t.alias()) @@ -268,17 +268,17 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_id("client_id"); /// - /// assert_eq!(oauth.contains(OAuthCredential::ClientId), true); - /// oauth.remove(OAuthCredential::ClientId); + /// assert_eq!(oauth.contains(OAuthParameter::ClientId), true); + /// oauth.remove(OAuthParameter::ClientId); /// - /// assert_eq!(oauth.contains(OAuthCredential::ClientId), false); + /// assert_eq!(oauth.contains(OAuthParameter::ClientId), false); /// ``` - pub fn remove(&mut self, oac: OAuthCredential) -> &mut OAuth { + pub fn remove(&mut self, oac: OAuthParameter) -> &mut OAuthSerializer { self.credentials.remove(oac.alias()); self } @@ -287,74 +287,74 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_id("client_id"); /// ``` - pub fn client_id(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::ClientId, value) + pub fn client_id(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ClientId, value) } /// Set the state for an OAuth request. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # use graph_oauth::oauth::OAuthCredential; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::oauth::OAuthParameter; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.state("1234"); /// ``` - pub fn state(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::State, value) + pub fn state(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::State, value) } /// Set the client secret for an OAuth request. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_secret("client_secret"); /// ``` - pub fn client_secret(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::ClientSecret, value) + pub fn client_secret(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ClientSecret, value) } /// Set the authorization URL. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.authorization_url("https://example.com/authorize"); /// ``` - pub fn authorization_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AuthorizationUrl, value) + pub fn authorization_url(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::AuthorizationUrl, value) } /// Set the access token url of a request for OAuth /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.access_token_url("https://example.com/token"); /// ``` - pub fn access_token_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AccessTokenUrl, value) + pub fn access_token_url(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::AccessTokenUrl, value) } /// Set the refresh token url of a request for OAuth /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.refresh_token_url("https://example.com/token"); /// ``` - pub fn refresh_token_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::RefreshTokenUrl, value) + pub fn refresh_token_url(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::RefreshTokenUrl, value) } /// Set the authorization, access token, and refresh token URL @@ -362,11 +362,11 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.tenant_id("tenant_id"); /// ``` - pub fn tenant_id(&mut self, value: &str) -> &mut OAuth { + pub fn tenant_id(&mut self, value: &str) -> &mut OAuthSerializer { let token_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/token",); let auth_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/authorize",); @@ -380,11 +380,15 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.tenant_id("tenant_id"); /// ``` - pub fn authority(&mut self, host: &AzureAuthorityHost, authority: &Authority) -> &mut OAuth { + pub fn authority( + &mut self, + host: &AzureAuthorityHost, + authority: &Authority, + ) -> &mut OAuthSerializer { if host.eq(&AzureAuthorityHost::OneDriveAndSharePoint) { return self.legacy_authority(); } @@ -405,7 +409,7 @@ impl OAuth { &mut self, host: &AzureAuthorityHost, authority: &Authority, - ) -> &mut OAuth { + ) -> &mut OAuthSerializer { let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); let auth_url = format!("{}/{}/adminconsent", host.as_ref(), authority.as_ref()); @@ -414,7 +418,7 @@ impl OAuth { .refresh_token_url(&token_url) } - pub fn legacy_authority(&mut self) -> &mut OAuth { + pub fn legacy_authority(&mut self) -> &mut OAuthSerializer { self.authorization_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); self.access_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); self.refresh_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()) @@ -424,61 +428,61 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.redirect_uri("https://localhost:8888/redirect"); /// ``` - pub fn redirect_uri(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::RedirectUri, value) + pub fn redirect_uri(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::RedirectUri, value) } /// Set the access code. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.authorization_code("LDSF[POK43"); /// ``` - pub fn authorization_code(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::AuthorizationCode, value) + pub fn authorization_code(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::AuthorizationCode, value) } /// Set the response mode. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.response_mode("query"); /// ``` - pub fn response_mode(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::ResponseMode, value) + pub fn response_mode(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ResponseMode, value) } /// Set the response type. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.response_type("token"); /// ``` - pub fn response_type<T: ToString>(&mut self, value: T) -> &mut OAuth { - self.insert(OAuthCredential::ResponseType, value) + pub fn response_type<T: ToString>(&mut self, value: T) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ResponseType, value) } /// Set the nonce. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; + /// # use graph_oauth::oauth::OAuthSerializer; /// - /// # let mut oauth = OAuth::new(); + /// # let mut oauth = OAuthSerializer::new(); /// oauth.nonce("1234"); /// ``` - pub fn nonce(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::Nonce, value) + pub fn nonce(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Nonce, value) } // rand = "0.8.5" @@ -487,118 +491,118 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; + /// # use graph_oauth::oauth::OAuthSerializer; /// - /// # let mut oauth = OAuth::new(); + /// # let mut oauth = OAuthSerializer::new(); /// oauth.prompt("login"); /// ``` - pub fn prompt(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::Prompt, value) + pub fn prompt(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Prompt, value) } /// Set id token for open id. /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuth, IdToken}; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::{OAuthSerializer, IdToken}; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.id_token(IdToken::new("1345", "code", "state", "session_state")); /// ``` - pub fn id_token(&mut self, value: IdToken) -> &mut OAuth { + pub fn id_token(&mut self, value: IdToken) -> &mut OAuthSerializer { if let Some(code) = value.get_code() { self.authorization_code(code.as_str()); } if let Some(state) = value.get_state() { - let _ = self.entry(OAuthCredential::State, state.as_str()); + let _ = self.entry(OAuthParameter::State, state.as_str()); } if let Some(session_state) = value.get_session_state() { self.session_state(session_state.as_str()); } - self.insert(OAuthCredential::IdToken, value.get_id_token().as_str()) + self.insert(OAuthParameter::IdToken, value.get_id_token().as_str()) } /// Set the session state. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.session_state("session-state"); /// ``` - pub fn session_state(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::SessionState, value) + pub fn session_state(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::SessionState, value) } /// Set the grant_type. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.grant_type("token"); /// ``` - pub fn grant_type(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::GrantType, value) + pub fn grant_type(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::GrantType, value) } /// Set the resource. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.resource("resource"); /// ``` - pub fn resource(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::Resource, value) + pub fn resource(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Resource, value) } /// Set the code verifier. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_verifier("code_verifier"); /// ``` - pub fn code_verifier(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::CodeVerifier, value) + pub fn code_verifier(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::CodeVerifier, value) } /// Set the domain hint. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.domain_hint("domain_hint"); /// ``` - pub fn domain_hint(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::DomainHint, value) + pub fn domain_hint(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::DomainHint, value) } /// Set the code challenge. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_challenge("code_challenge"); /// ``` - pub fn code_challenge(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::CodeChallenge, value) + pub fn code_challenge(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::CodeChallenge, value) } /// Set the code challenge method. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_challenge_method("code_challenge_method"); /// ``` - pub fn code_challenge_method(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::CodeChallengeMethod, value) + pub fn code_challenge_method(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::CodeChallengeMethod, value) } /// Generate a code challenge and code verifier for the @@ -622,22 +626,22 @@ impl OAuth { /// # Example /// ``` /// # use base64::Engine; - /// use graph_oauth::oauth::OAuth; - /// use graph_oauth::oauth::OAuthCredential; + /// use graph_oauth::oauth::OAuthSerializer; + /// use graph_oauth::oauth::OAuthParameter; /// - /// let mut oauth = OAuth::new(); + /// let mut oauth = OAuthSerializer::new(); /// oauth.generate_sha256_challenge_and_verifier().unwrap(); /// - /// # assert!(oauth.contains(OAuthCredential::CodeChallenge)); - /// # assert!(oauth.contains(OAuthCredential::CodeVerifier)); - /// # assert!(oauth.contains(OAuthCredential::CodeChallengeMethod)); - /// println!("Code Challenge: {:#?}", oauth.get(OAuthCredential::CodeChallenge)); - /// println!("Code Verifier: {:#?}", oauth.get(OAuthCredential::CodeVerifier)); - /// println!("Code Challenge Method: {:#?}", oauth.get(OAuthCredential::CodeChallengeMethod)); + /// # assert!(oauth.contains(OAuthParameter::CodeChallenge)); + /// # assert!(oauth.contains(OAuthParameter::CodeVerifier)); + /// # assert!(oauth.contains(OAuthParameter::CodeChallengeMethod)); + /// println!("Code Challenge: {:#?}", oauth.get(OAuthParameter::CodeChallenge)); + /// println!("Code Verifier: {:#?}", oauth.get(OAuthParameter::CodeVerifier)); + /// println!("Code Challenge Method: {:#?}", oauth.get(OAuthParameter::CodeChallengeMethod)); /// - /// # let challenge = oauth.get(OAuthCredential::CodeChallenge).unwrap(); + /// # let challenge = oauth.get(OAuthParameter::CodeChallenge).unwrap(); /// # let mut context = ring::digest::Context::new(&ring::digest::SHA256); - /// # context.update(oauth.get(OAuthCredential::CodeVerifier).unwrap().as_bytes()); + /// # context.update(oauth.get(OAuthParameter::CodeVerifier).unwrap().as_bytes()); /// # let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); /// # assert_eq!(challenge, verifier); /// ``` @@ -661,118 +665,118 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.login_hint("login_hint"); /// ``` - pub fn login_hint(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::LoginHint, value) + pub fn login_hint(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::LoginHint, value) } /// Set the client assertion. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_assertion("client_assertion"); /// ``` - pub fn client_assertion(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::ClientAssertion, value) + pub fn client_assertion(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ClientAssertion, value) } /// Set the client assertion type. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_assertion_type("client_assertion_type"); /// ``` - pub fn client_assertion_type(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::ClientAssertionType, value) + pub fn client_assertion_type(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ClientAssertionType, value) } /// Set the url to send a post request that will log out the user. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.logout_url("https://example.com/logout?"); /// ``` - pub fn logout_url(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::LogoutURL, value) + pub fn logout_url(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::LogoutURL, value) } /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.post_logout_redirect_uri("http://localhost:8080"); /// ``` - pub fn post_logout_redirect_uri(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::PostLogoutRedirectURI, value) + pub fn post_logout_redirect_uri(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::PostLogoutRedirectURI, value) } /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuth, OAuthCredential}; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.username("user"); - /// assert!(oauth.contains(OAuthCredential::Username)) + /// assert!(oauth.contains(OAuthParameter::Username)) /// ``` - pub fn username(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::Username, value) + pub fn username(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Username, value) } /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuth, OAuthCredential}; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.password("user"); - /// assert!(oauth.contains(OAuthCredential::Password)) + /// assert!(oauth.contains(OAuthParameter::Password)) /// ``` - pub fn password(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::Password, value) + pub fn password(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Password, value) } - pub fn refresh_token(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::RefreshToken, value) + pub fn refresh_token(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::RefreshToken, value) } /// Set the device code for the device authorization flow. /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuth, OAuthCredential}; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # let mut oauth = OAuthSerializer::new(); /// oauth.device_code("device_code"); - /// assert!(oauth.contains(OAuthCredential::DeviceCode)) + /// assert!(oauth.contains(OAuthParameter::DeviceCode)) /// ``` - pub fn device_code(&mut self, value: &str) -> &mut OAuth { - self.insert(OAuthCredential::DeviceCode, value) + pub fn device_code(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::DeviceCode, value) } /// Add a scope' for the OAuth URL. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Sites.Read") /// .add_scope("Sites.ReadWrite") /// .add_scope("Sites.ReadWrite.All"); /// assert_eq!(oauth.join_scopes(" "), "Sites.Read Sites.ReadWrite Sites.ReadWrite.All"); /// ``` - pub fn add_scope<T: ToString>(&mut self, scope: T) -> &mut OAuth { + pub fn add_scope<T: ToString>(&mut self, scope: T) -> &mut OAuthSerializer { self.scopes.insert(scope.to_string()); self } @@ -781,8 +785,8 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// let mut oauth = OAuthSerializer::new(); /// oauth.add_scope("Files.Read"); /// oauth.add_scope("Files.ReadWrite"); /// @@ -798,8 +802,8 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// /// // the scopes take a separator just like Vec join. /// let s = oauth.join_scopes(" "); @@ -817,9 +821,9 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; + /// # use graph_oauth::oauth::OAuthSerializer; /// # use std::collections::HashSet; - /// # let mut oauth = OAuth::new(); + /// # let mut oauth = OAuthSerializer::new(); /// /// let scopes1 = vec!["Files.Read", "Files.ReadWrite"]; /// oauth.extend_scopes(&scopes1); @@ -835,8 +839,8 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Files.Read"); /// assert_eq!(oauth.contains_scope("Files.Read"), true); @@ -853,8 +857,8 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("scope"); /// # assert!(oauth.contains_scope("scope")); @@ -869,8 +873,8 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Files.Read").add_scope("Files.ReadWrite"); /// assert_eq!("Files.Read Files.ReadWrite", oauth.join_scopes(" ")); @@ -886,9 +890,9 @@ impl OAuth { /// /// # Example /// ``` - /// use graph_oauth::oauth::OAuth; + /// use graph_oauth::oauth::OAuthSerializer; /// use graph_oauth::oauth::AccessToken; - /// let mut oauth = OAuth::new(); + /// let mut oauth = OAuthSerializer::new(); /// let access_token = AccessToken::default(); /// oauth.access_token(access_token); /// ``` @@ -903,10 +907,10 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; + /// # use graph_oauth::oauth::OAuthSerializer; /// # use graph_oauth::oauth::AccessToken; /// # let access_token = AccessToken::default(); - /// # let mut oauth = OAuth::new(); + /// # let mut oauth = OAuthSerializer::new(); /// # oauth.access_token(access_token); /// let access_token = oauth.get_access_token().unwrap(); /// println!("{:#?}", access_token); @@ -921,9 +925,9 @@ impl OAuth { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; + /// # use graph_oauth::oauth::OAuthSerializer; /// # use graph_oauth::oauth::AccessToken; - /// # let mut oauth = OAuth::new(); + /// # let mut oauth = OAuthSerializer::new(); /// let mut access_token = AccessToken::default(); /// access_token.set_refresh_token("refresh_token"); /// oauth.access_token(access_token); @@ -932,16 +936,16 @@ impl OAuth { /// println!("{:#?}", refresh_token); /// ``` pub fn get_refresh_token(&self) -> GraphResult<String> { - if let Some(refresh_token) = self.get(OAuthCredential::RefreshToken) { + if let Some(refresh_token) = self.get(OAuthParameter::RefreshToken) { return Ok(refresh_token); } match self.get_access_token() { Some(token) => match token.refresh_token() { Some(t) => Ok(t), - None => OAuthError::error_from::<String>(OAuthCredential::RefreshToken), + None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), }, - None => OAuthError::error_from::<String>(OAuthCredential::RefreshToken), + None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), } } @@ -969,7 +973,7 @@ impl OAuth { /// oauth.v1_logout().unwrap(); /// ``` pub fn v1_logout(&mut self) -> GraphResult<()> { - let mut url = self.get_or_else(OAuthCredential::LogoutURL)?; + let mut url = self.get_or_else(OAuthParameter::LogoutURL)?; if !url.ends_with('?') { url.push('?'); } @@ -977,13 +981,13 @@ impl OAuth { let mut vec = vec![ url, "&client_id=".to_string(), - self.get_or_else(OAuthCredential::ClientId)?, + self.get_or_else(OAuthParameter::ClientId)?, "&redirect_uri=".to_string(), ]; - if let Some(redirect) = self.get(OAuthCredential::PostLogoutRedirectURI) { + if let Some(redirect) = self.get(OAuthParameter::PostLogoutRedirectURI) { vec.push(redirect); - } else if let Some(redirect) = self.get(OAuthCredential::RedirectUri) { + } else if let Some(redirect) = self.get(OAuthParameter::RedirectUri) { vec.push(redirect); } webbrowser::open(vec.join("").as_str()).map_err(GraphFailure::from) @@ -999,15 +1003,15 @@ impl OAuth { /// oauth.v2_logout().unwrap(); /// ``` pub fn v2_logout(&self) -> GraphResult<()> { - let mut url = self.get_or_else(OAuthCredential::LogoutURL)?; + let mut url = self.get_or_else(OAuthParameter::LogoutURL)?; if !url.ends_with('?') { url.push('?'); } - if let Some(redirect) = self.get(OAuthCredential::PostLogoutRedirectURI) { + if let Some(redirect) = self.get(OAuthParameter::PostLogoutRedirectURI) { url.push_str("post_logout_redirect_uri="); url.push_str(redirect.as_str()); } else { - let redirect_uri = self.get_or_else(OAuthCredential::RedirectUri)?; + let redirect_uri = self.get_or_else(OAuthParameter::RedirectUri)?; url.push_str("post_logout_redirect_uri="); url.push_str(redirect_uri.as_str()); } @@ -1015,14 +1019,14 @@ impl OAuth { } } -impl OAuth { - pub fn get_or_else(&self, c: OAuthCredential) -> GraphResult<String> { +impl OAuthSerializer { + pub fn get_or_else(&self, c: OAuthParameter) -> GraphResult<String> { self.get(c).ok_or_else(|| OAuthError::credential_error(c)) } pub fn form_encode_credentials( &mut self, - pairs: Vec<OAuthCredential>, + pairs: Vec<OAuthParameter>, encoder: &mut Serializer<String>, ) { pairs @@ -1037,11 +1041,11 @@ impl OAuth { }); } - fn query_encode_filter(&self, form_credential: &FormCredential) -> bool { + fn query_encode_filter(&self, form_credential: &SerializerField) -> bool { let oac = { match form_credential { - FormCredential::Required(oac) => *oac, - FormCredential::NotRequired(oac) => *oac, + SerializerField::Required(oac) => *oac, + SerializerField::NotRequired(oac) => *oac, } }; self.contains_key(oac.alias()) || oac.alias().eq("scope") @@ -1049,13 +1053,13 @@ impl OAuth { pub fn url_query_encode( &mut self, - pairs: Vec<FormCredential>, + pairs: Vec<SerializerField>, encoder: &mut Serializer<String>, ) -> AuthorizationResult<()> { for form_credential in pairs.iter() { if self.query_encode_filter(form_credential) { match form_credential { - FormCredential::Required(oac) => { + SerializerField::Required(oac) => { if oac.alias().eq("scope") { if self.scopes.is_empty() { return AuthorizationFailure::required_value_msg_result::<()>( @@ -1074,7 +1078,7 @@ impl OAuth { ); } } - FormCredential::NotRequired(oac) => { + SerializerField::NotRequired(oac) => { if oac.alias().eq("scope") && !self.scopes.is_empty() { encoder.append_pair("scope", self.join_scopes(" ").as_str()); } else if let Some(val) = self.get(*oac) { @@ -1088,10 +1092,10 @@ impl OAuth { Ok(()) } - pub fn params(&mut self, pairs: Vec<OAuthCredential>) -> GraphResult<HashMap<String, String>> { + pub fn params(&mut self, pairs: Vec<OAuthParameter>) -> GraphResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for oac in pairs.iter() { - if oac.eq(&OAuthCredential::RefreshToken) { + if oac.eq(&OAuthParameter::RefreshToken) { if let Some(val) = self.get(*oac) { map.insert(oac.to_string(), val); } else { @@ -1108,13 +1112,13 @@ impl OAuth { pub fn authorization_form( &mut self, - form_credentials: Vec<FormCredential>, + form_credentials: Vec<SerializerField>, ) -> AuthorizationResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for form_credential in form_credentials.iter() { match form_credential { - FormCredential::Required(oac) => { + SerializerField::Required(oac) => { let val = self.get(*oac).ok_or(AuthorizationFailure::RequiredValue { name: oac.alias().into(), message: None, @@ -1125,8 +1129,8 @@ impl OAuth { map.insert(oac.to_string(), val); } } - FormCredential::NotRequired(oac) => { - if oac.eq(&OAuthCredential::Scope) && !self.scopes.is_empty() { + SerializerField::NotRequired(oac) => { + if oac.eq(&OAuthParameter::Scope) && !self.scopes.is_empty() { map.insert("scope".into(), self.join_scopes(" ")); } else if let Some(val) = self.get(*oac) { if !val.trim().is_empty() { @@ -1150,9 +1154,9 @@ impl OAuth { GrantType::TokenFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthCredential::ResponseType, "token"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); self.form_encode_credentials(GrantType::TokenFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1170,11 +1174,11 @@ impl OAuth { GrantType::CodeFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthCredential::ResponseType, "code"); - let _ = self.entry(OAuthCredential::ResponseMode, "query"); + let _ = self.entry(OAuthParameter::ResponseType, "code"); + let _ = self.entry(OAuthParameter::ResponseMode, "query"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1182,13 +1186,13 @@ impl OAuth { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthCredential::ResponseType, "token"); - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1196,10 +1200,10 @@ impl OAuth { GrantType::AuthorizationCode => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthCredential::ResponseType, "code"); - let _ = self.entry(OAuthCredential::ResponseMode, "query"); + let _ = self.entry(OAuthParameter::ResponseType, "code"); + let _ = self.entry(OAuthParameter::ResponseMode, "query"); self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1208,9 +1212,9 @@ impl OAuth { } GrantRequest::AccessToken | GrantRequest::RefreshToken => { if request_type == GrantRequest::AccessToken { - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); } else { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(request_type), &mut encoder); Ok(encoder.finish()) @@ -1220,10 +1224,10 @@ impl OAuth { match request_type { GrantRequest::Authorization => { if !self.scopes.is_empty() { - let _ = self.entry(OAuthCredential::ResponseType, "token"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); } self.form_encode_credentials(GrantType::Implicit.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1243,7 +1247,7 @@ impl OAuth { GrantRequest::Authorization => { self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1251,12 +1255,12 @@ impl OAuth { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthCredential::GrantType, "urn:ietf:params:oauth:grant-type:device_code"); + let _ = self.entry(OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code"); self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } @@ -1266,7 +1270,7 @@ impl OAuth { GrantRequest::Authorization => { self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1274,12 +1278,12 @@ impl OAuth { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1288,7 +1292,7 @@ impl OAuth { match request_type { GrantRequest::Authorization => { self.form_encode_credentials(GrantType::ClientCredentials.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthCredential::AuthorizationUrl)?; + let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { url.push('?'); } @@ -1313,69 +1317,69 @@ impl OAuth { match grant { GrantType::TokenFlow => { if request_type.eq(&GrantRequest::Authorization) { - let _ = self.entry(OAuthCredential::ResponseType, "token"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); } } GrantType::CodeFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthCredential::ResponseType, "code"); - let _ = self.entry(OAuthCredential::ResponseMode, "query"); + let _ = self.entry(OAuthParameter::ResponseType, "code"); + let _ = self.entry(OAuthParameter::ResponseMode, "query"); } GrantRequest::AccessToken => { - let _ = self.entry(OAuthCredential::ResponseType, "token"); - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } }, GrantType::AuthorizationCode => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthCredential::ResponseType, "code"); - let _ = self.entry(OAuthCredential::ResponseMode, "query"); + let _ = self.entry(OAuthParameter::ResponseType, "code"); + let _ = self.entry(OAuthParameter::ResponseMode, "query"); } GrantRequest::AccessToken | GrantRequest::RefreshToken => { if request_type == GrantRequest::AccessToken { - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); } else { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } } }, GrantType::Implicit => { if request_type.eq(&GrantRequest::Authorization) && !self.scopes.is_empty() { - let _ = self.entry(OAuthCredential::ResponseType, "token"); + let _ = self.entry(OAuthParameter::ResponseType, "token"); } } GrantType::DeviceCode => { if request_type.eq(&GrantRequest::AccessToken) { let _ = self.entry( - OAuthCredential::GrantType, + OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code", ); } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } } GrantType::OpenId => { if request_type.eq(&GrantRequest::AccessToken) { - let _ = self.entry(OAuthCredential::GrantType, "authorization_code"); + let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } } GrantType::ClientCredentials => { if request_type.eq(&GrantRequest::AccessToken) || request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthCredential::GrantType, "client_credentials"); + let _ = self.entry(OAuthParameter::GrantType, "client_credentials"); } } GrantType::ResourceOwnerPasswordCredentials => { if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthCredential::GrantType, "refresh_token"); + let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); } else { - let _ = self.entry(OAuthCredential::GrantType, "password"); + let _ = self.entry(OAuthParameter::GrantType, "password"); } } } @@ -1386,19 +1390,19 @@ impl OAuth { /// /// # Example /// ``` -/// # use graph_oauth::oauth::{OAuth, OAuthCredential}; +/// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; /// # use std::collections::HashMap; -/// # let mut oauth = OAuth::new(); -/// let mut map: HashMap<OAuthCredential, &str> = HashMap::new(); -/// map.insert(OAuthCredential::ClientId, "client_id"); -/// map.insert(OAuthCredential::ClientSecret, "client_secret"); +/// # let mut oauth = OAuthSerializer::new(); +/// let mut map: HashMap<OAuthParameter, &str> = HashMap::new(); +/// map.insert(OAuthParameter::ClientId, "client_id"); +/// map.insert(OAuthParameter::ClientSecret, "client_secret"); /// /// oauth.extend(map); -/// # assert_eq!(oauth.get(OAuthCredential::ClientId), Some("client_id".to_string())); -/// # assert_eq!(oauth.get(OAuthCredential::ClientSecret), Some("client_secret".to_string())); +/// # assert_eq!(oauth.get(OAuthParameter::ClientId), Some("client_id".to_string())); +/// # assert_eq!(oauth.get(OAuthParameter::ClientSecret), Some("client_secret".to_string())); /// ``` -impl<V: ToString> Extend<(OAuthCredential, V)> for OAuth { - fn extend<I: IntoIterator<Item = (OAuthCredential, V)>>(&mut self, iter: I) { +impl<V: ToString> Extend<(OAuthParameter, V)> for OAuthSerializer { + fn extend<I: IntoIterator<Item = (OAuthParameter, V)>>(&mut self, iter: I) { iter.into_iter().for_each(|entry| { self.insert(entry.0, entry.1); }); @@ -1406,7 +1410,7 @@ impl<V: ToString> Extend<(OAuthCredential, V)> for OAuth { } pub struct GrantSelector<T> { - oauth: OAuth, + oauth: OAuthSerializer, t: PhantomData<T>, } @@ -1418,8 +1422,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().token_flow(); /// ``` pub fn token_flow(self) -> ImplicitGrant { @@ -1436,8 +1440,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().code_flow(); /// ``` pub fn code_flow(self) -> AccessTokenGrant { @@ -1454,8 +1458,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().implicit_grant(); /// ``` pub fn implicit_grant(self) -> ImplicitGrant { @@ -1472,8 +1476,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().authorization_code_grant(); /// ``` pub fn authorization_code_grant(self) -> AccessTokenGrant { @@ -1490,8 +1494,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let device_code_handler = oauth.build().device_code(); /// ``` pub fn device_code(self) -> DeviceCodeGrant { @@ -1508,8 +1512,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().open_id_connect(); /// ``` pub fn open_id_connect(self) -> AccessTokenGrant { @@ -1526,8 +1530,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().client_credentials(); /// ``` pub fn client_credentials(self) -> AccessTokenGrant { @@ -1544,8 +1548,8 @@ impl GrantSelector<AccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().resource_owner_password_credentials(); /// ``` pub fn resource_owner_password_credentials(self) -> AccessTokenGrant { @@ -1564,8 +1568,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().token_flow(); /// ``` pub fn token_flow(self) -> ImplicitGrant { @@ -1582,8 +1586,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().code_flow(); /// ``` pub fn code_flow(self) -> AsyncAccessTokenGrant { @@ -1600,8 +1604,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().implicit_grant(); /// ``` pub fn implicit_grant(self) -> ImplicitGrant { @@ -1618,8 +1622,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().authorization_code_grant(); /// ``` pub fn authorization_code_grant(self) -> AsyncAccessTokenGrant { @@ -1636,8 +1640,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let device_code_handler = oauth.build().device_code(); /// ``` pub fn device_code(self) -> AsyncDeviceCodeGrant { @@ -1654,8 +1658,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().open_id_connect(); /// ``` pub fn open_id_connect(self) -> AsyncAccessTokenGrant { @@ -1672,8 +1676,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().client_credentials(); /// ``` pub fn client_credentials(self) -> AsyncAccessTokenGrant { @@ -1690,8 +1694,8 @@ impl GrantSelector<AsyncAccessTokenGrant> { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuth; - /// # let mut oauth = OAuth::new(); + /// # use graph_oauth::oauth::OAuthSerializer; + /// # let mut oauth = OAuthSerializer::new(); /// let open_id = oauth.build().resource_owner_password_credentials(); /// ``` pub fn resource_owner_password_credentials(self) -> AsyncAccessTokenGrant { @@ -1728,7 +1732,7 @@ pub struct AccessTokenRequest { impl AccessTokenRequest { /// Send the request for an access token. If successful, the Response body /// should be an access token which you can convert to [AccessToken] - /// and pass back to [OAuth] to use to get refresh tokens. + /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example /// ```rust,ignore @@ -1807,7 +1811,7 @@ pub struct AsyncAccessTokenRequest { impl AsyncAccessTokenRequest { /// Send the request for an access token. If successful, the Response body /// should be an access token which you can convert to [AccessToken] - /// and pass back to [OAuth] to use to get refresh tokens. + /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example /// ```rust,ignore @@ -1879,7 +1883,7 @@ impl AsyncAccessTokenRequest { #[derive(Debug)] pub struct ImplicitGrant { - oauth: OAuth, + oauth: OAuthSerializer, grant: GrantType, } @@ -1889,7 +1893,7 @@ impl ImplicitGrant { .pre_request_check(self.grant, GrantRequest::Authorization); Ok(Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizationUrl)? + .get_or_else(OAuthParameter::AuthorizationUrl)? .as_str(), )?) } @@ -1925,20 +1929,20 @@ impl ImplicitGrant { } } -impl From<ImplicitGrant> for OAuth { +impl From<ImplicitGrant> for OAuthSerializer { fn from(token_grant: ImplicitGrant) -> Self { token_grant.oauth } } -impl AsRef<OAuth> for ImplicitGrant { - fn as_ref(&self) -> &OAuth { +impl AsRef<OAuthSerializer> for ImplicitGrant { + fn as_ref(&self) -> &OAuthSerializer { &self.oauth } } pub struct DeviceCodeGrant { - oauth: OAuth, + oauth: OAuthSerializer, grant: GrantType, } @@ -1952,7 +1956,7 @@ impl DeviceCodeGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizationUrl)? + .get_or_else(OAuthParameter::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -1962,7 +1966,7 @@ impl DeviceCodeGrant { pub fn authorization(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::Authorization); - let uri = self.oauth.get_or_else(OAuthCredential::AuthorizationUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AuthorizationUrl); let params = self.oauth.params( self.grant .available_credentials(GrantRequest::Authorization), @@ -1994,7 +1998,7 @@ impl DeviceCodeGrant { pub fn access_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2025,7 +2029,7 @@ impl DeviceCodeGrant { pub fn refresh_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); @@ -2055,7 +2059,7 @@ impl DeviceCodeGrant { } pub struct AsyncDeviceCodeGrant { - oauth: OAuth, + oauth: OAuthSerializer, grant: GrantType, } @@ -2069,7 +2073,7 @@ impl AsyncDeviceCodeGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizationUrl)? + .get_or_else(OAuthParameter::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -2079,7 +2083,7 @@ impl AsyncDeviceCodeGrant { pub fn authorization(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::Authorization); - let uri = self.oauth.get_or_else(OAuthCredential::AuthorizationUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AuthorizationUrl); let params = self.oauth.params( self.grant .available_credentials(GrantRequest::Authorization), @@ -2111,7 +2115,7 @@ impl AsyncDeviceCodeGrant { pub fn access_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2142,7 +2146,7 @@ impl AsyncDeviceCodeGrant { pub fn refresh_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); @@ -2173,7 +2177,7 @@ impl AsyncDeviceCodeGrant { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AccessTokenGrant { - oauth: OAuth, + oauth: OAuthSerializer, grant: GrantType, } @@ -2187,7 +2191,7 @@ impl AccessTokenGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizationUrl)? + .get_or_else(OAuthParameter::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -2272,7 +2276,7 @@ impl AccessTokenGrant { pub fn access_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2305,7 +2309,7 @@ impl AccessTokenGrant { pub fn refresh_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); @@ -2336,7 +2340,7 @@ impl AccessTokenGrant { #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AsyncAccessTokenGrant { - oauth: OAuth, + oauth: OAuthSerializer, grant: GrantType, } @@ -2350,7 +2354,7 @@ impl AsyncAccessTokenGrant { )?; let mut url = Url::parse( self.oauth - .get_or_else(OAuthCredential::AuthorizationUrl)? + .get_or_else(OAuthParameter::AuthorizationUrl)? .as_str(), )?; url.query_pairs_mut().extend_pairs(¶ms); @@ -2436,7 +2440,7 @@ impl AsyncAccessTokenGrant { pub fn access_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthCredential::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2469,7 +2473,7 @@ impl AsyncAccessTokenGrant { pub fn refresh_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthCredential::RefreshTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::RefreshToken)); @@ -2498,29 +2502,29 @@ impl AsyncAccessTokenGrant { } } -impl From<AccessTokenGrant> for OAuth { +impl From<AccessTokenGrant> for OAuthSerializer { fn from(token_grant: AccessTokenGrant) -> Self { token_grant.oauth } } -impl AsRef<OAuth> for AccessTokenGrant { - fn as_ref(&self) -> &OAuth { +impl AsRef<OAuthSerializer> for AccessTokenGrant { + fn as_ref(&self) -> &OAuthSerializer { &self.oauth } } -impl AsMut<OAuth> for AccessTokenGrant { - fn as_mut(&mut self) -> &mut OAuth { +impl AsMut<OAuthSerializer> for AccessTokenGrant { + fn as_mut(&mut self) -> &mut OAuthSerializer { &mut self.oauth } } -impl fmt::Debug for OAuth { +impl fmt::Debug for OAuthSerializer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut map_debug: BTreeMap<&str, &str> = BTreeMap::new(); for (key, value) in self.credentials.iter() { - if let Some(oac) = OAuthCredential::iter() + if let Some(oac) = OAuthParameter::iter() .find(|oac| oac.alias().eq(key.as_str()) && oac.is_debug_redacted()) { map_debug.insert(oac.alias(), "[REDACTED]"); diff --git a/graph-oauth/src/auth_response_query.rs b/graph-oauth/src/auth_response_query.rs new file mode 100644 index 00000000..bb958a52 --- /dev/null +++ b/graph-oauth/src/auth_response_query.rs @@ -0,0 +1,23 @@ +/* + let code = query.get("code"); + let id_token = query.get("id_token"); + let access_token = query.get("access_token"); + let state = query.get("state"); + let nonce = query.get("nonce"); +*/ + +use serde_json::Value; +use std::collections::HashMap; + +#[derive(Clone, Serialize, Deserialize)] +pub struct AuthResponseQuery { + pub code: Option<String>, + pub id_token: Option<String>, + pub access_token: Option<String>, + pub state: Option<String>, + pub nonce: Option<String>, + #[serde(flatten)] + additional_fields: HashMap<String, Value>, + #[serde(skip)] + log_pii: bool, +} diff --git a/graph-oauth/src/discovery/graph_discovery.rs b/graph-oauth/src/discovery/graph_discovery.rs index 6c16bc01..2c5e7bf8 100644 --- a/graph-oauth/src/discovery/graph_discovery.rs +++ b/graph-oauth/src/discovery/graph_discovery.rs @@ -1,5 +1,5 @@ use crate::oauth::well_known::WellKnown; -use crate::oauth::{OAuth, OAuthError}; +use crate::oauth::{OAuthError, OAuthSerializer}; static LOGIN_LIVE_HOST: &str = "https://login.live.com"; static MICROSOFT_ONLINE_HOST: &str = "https://login.microsoftonline.com"; @@ -122,8 +122,8 @@ impl GraphDiscovery { /// let oauth = GraphDiscovery::V1.oauth().unwrap(); /// println!("{:#?}", oauth); /// ``` - pub fn oauth(self) -> Result<OAuth, OAuthError> { - let mut oauth = OAuth::new(); + pub fn oauth(self) -> Result<OAuthSerializer, OAuthError> { + let mut oauth = OAuthSerializer::new(); match self { GraphDiscovery::V1 => { let k: MicrosoftSigningKeysV1 = self.signing_keys()?; @@ -156,8 +156,8 @@ impl GraphDiscovery { /// let oauth = GraphDiscovery::V1.async_oauth().await.unwrap(); /// println!("{:#?}", oauth); /// ``` - pub async fn async_oauth(self) -> Result<OAuth, OAuthError> { - let mut oauth = OAuth::new(); + pub async fn async_oauth(self) -> Result<OAuthSerializer, OAuthError> { + let mut oauth = OAuthSerializer::new(); match self { GraphDiscovery::V1 => { let k: MicrosoftSigningKeysV1 = self.async_signing_keys().await?; diff --git a/graph-oauth/src/grants.rs b/graph-oauth/src/grants.rs index 97a043bb..d86fa3ac 100644 --- a/graph-oauth/src/grants.rs +++ b/graph-oauth/src/grants.rs @@ -1,4 +1,4 @@ -use crate::auth::OAuthCredential; +use crate::auth::OAuthParameter; #[derive( Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, @@ -24,164 +24,164 @@ pub enum GrantType { } impl GrantType { - pub fn available_credentials(self, grant_request: GrantRequest) -> Vec<OAuthCredential> { + pub fn available_credentials(self, grant_request: GrantRequest) -> Vec<OAuthParameter> { match self { GrantType::TokenFlow => match grant_request { GrantRequest::Authorization | GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::ResponseType, - OAuthCredential::Scope, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::ResponseType, + OAuthParameter::Scope, ], }, GrantType::CodeFlow => match grant_request { GrantRequest::Authorization => vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::State, - OAuthCredential::ResponseType, - OAuthCredential::Scope, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::State, + OAuthParameter::ResponseType, + OAuthParameter::Scope, ], GrantRequest::AccessToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RedirectUri, - OAuthCredential::ResponseType, - OAuthCredential::GrantType, - OAuthCredential::AuthorizationCode, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::ResponseType, + OAuthParameter::GrantType, + OAuthParameter::AuthorizationCode, ], GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RedirectUri, - OAuthCredential::GrantType, - OAuthCredential::AuthorizationCode, - OAuthCredential::RefreshToken, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::GrantType, + OAuthParameter::AuthorizationCode, + OAuthParameter::RefreshToken, ], }, GrantType::AuthorizationCode => match grant_request { GrantRequest::Authorization => vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::State, - OAuthCredential::ResponseMode, - OAuthCredential::ResponseType, - OAuthCredential::Scope, - OAuthCredential::Prompt, - OAuthCredential::DomainHint, - OAuthCredential::LoginHint, - OAuthCredential::CodeChallenge, - OAuthCredential::CodeChallengeMethod, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::State, + OAuthParameter::ResponseMode, + OAuthParameter::ResponseType, + OAuthParameter::Scope, + OAuthParameter::Prompt, + OAuthParameter::DomainHint, + OAuthParameter::LoginHint, + OAuthParameter::CodeChallenge, + OAuthParameter::CodeChallengeMethod, ], GrantRequest::AccessToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RedirectUri, - OAuthCredential::AuthorizationCode, - OAuthCredential::Scope, - OAuthCredential::GrantType, - OAuthCredential::CodeVerifier, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::AuthorizationCode, + OAuthParameter::Scope, + OAuthParameter::GrantType, + OAuthParameter::CodeVerifier, ], GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RefreshToken, - OAuthCredential::GrantType, - OAuthCredential::Scope, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RefreshToken, + OAuthParameter::GrantType, + OAuthParameter::Scope, ], }, GrantType::Implicit => match grant_request { GrantRequest::Authorization | GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::Scope, - OAuthCredential::ResponseType, - OAuthCredential::ResponseMode, - OAuthCredential::State, - OAuthCredential::Nonce, - OAuthCredential::Prompt, - OAuthCredential::LoginHint, - OAuthCredential::DomainHint, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::Scope, + OAuthParameter::ResponseType, + OAuthParameter::ResponseMode, + OAuthParameter::State, + OAuthParameter::Nonce, + OAuthParameter::Prompt, + OAuthParameter::LoginHint, + OAuthParameter::DomainHint, ], }, GrantType::OpenId => match grant_request { GrantRequest::Authorization => vec![ - OAuthCredential::ClientId, - OAuthCredential::ResponseType, - OAuthCredential::RedirectUri, - OAuthCredential::ResponseMode, - OAuthCredential::Scope, - OAuthCredential::State, - OAuthCredential::Nonce, - OAuthCredential::Prompt, - OAuthCredential::LoginHint, - OAuthCredential::DomainHint, - OAuthCredential::Resource, + OAuthParameter::ClientId, + OAuthParameter::ResponseType, + OAuthParameter::RedirectUri, + OAuthParameter::ResponseMode, + OAuthParameter::Scope, + OAuthParameter::State, + OAuthParameter::Nonce, + OAuthParameter::Prompt, + OAuthParameter::LoginHint, + OAuthParameter::DomainHint, + OAuthParameter::Resource, ], GrantRequest::AccessToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RedirectUri, - OAuthCredential::GrantType, - OAuthCredential::Scope, - OAuthCredential::AuthorizationCode, - OAuthCredential::CodeVerifier, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::GrantType, + OAuthParameter::Scope, + OAuthParameter::AuthorizationCode, + OAuthParameter::CodeVerifier, ], GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::RefreshToken, - OAuthCredential::GrantType, - OAuthCredential::Scope, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RefreshToken, + OAuthParameter::GrantType, + OAuthParameter::Scope, ], }, GrantType::ClientCredentials => match grant_request { GrantRequest::Authorization => vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::State, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::State, ], GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::GrantType, - OAuthCredential::Scope, - OAuthCredential::ClientAssertion, - OAuthCredential::ClientAssertionType, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::GrantType, + OAuthParameter::Scope, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, ], }, GrantType::ResourceOwnerPasswordCredentials => match grant_request { GrantRequest::Authorization | GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::ClientSecret, - OAuthCredential::GrantType, - OAuthCredential::Username, - OAuthCredential::Password, - OAuthCredential::Scope, - OAuthCredential::RedirectUri, - OAuthCredential::ClientAssertion, + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::GrantType, + OAuthParameter::Username, + OAuthParameter::Password, + OAuthParameter::Scope, + OAuthParameter::RedirectUri, + OAuthParameter::ClientAssertion, ], }, GrantType::DeviceCode => match grant_request { GrantRequest::Authorization => { - vec![OAuthCredential::ClientId, OAuthCredential::Scope] + vec![OAuthParameter::ClientId, OAuthParameter::Scope] } GrantRequest::AccessToken => vec![ - OAuthCredential::GrantType, - OAuthCredential::ClientId, - OAuthCredential::DeviceCode, + OAuthParameter::GrantType, + OAuthParameter::ClientId, + OAuthParameter::DeviceCode, ], GrantRequest::RefreshToken => vec![ - OAuthCredential::ClientId, - OAuthCredential::Scope, - OAuthCredential::GrantType, - OAuthCredential::RefreshToken, + OAuthParameter::ClientId, + OAuthParameter::Scope, + OAuthParameter::GrantType, + OAuthParameter::RefreshToken, ], }, } diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index 717365d3..870cd379 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -1,5 +1,5 @@ use crate::jwt::{JsonWebToken, JwtParser}; -use serde::de::Visitor; +use serde::de::{Error, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; use std::borrow::Cow; @@ -158,6 +158,13 @@ impl<'de> Deserialize<'de> for IdToken { } Ok(id_token) } + + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + IdToken::from_str(v).map_err(|err| Error::custom(err)) + } } deserializer.deserialize_identifier(IdTokenVisitor) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 82e8b0c4..58408eec 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,12 +1,15 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::{Authority, AuthorizationUrl, AzureAuthorityHost, Prompt, ResponseMode}; -use crate::oauth::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::auth_response_query::AuthResponseQuery; +use crate::identity::{ + Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, +}; +use crate::oauth::form_credential::SerializerField; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; -use crate::web::InteractiveAuthenticator; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; +use crate::web::{InteractiveAuthenticator, InteractiveWebViewOptions}; + use graph_error::{AuthorizationFailure, AuthorizationResult}; -use ring::rand::SecureRandom; + +use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; @@ -29,7 +32,7 @@ pub struct AuthCodeAuthorizationUrl { pub(crate) client_id: String, pub(crate) redirect_uri: String, pub(crate) authority: Authority, - pub(crate) response_type: Vec<ResponseType>, + pub(crate) response_type: BTreeSet<ResponseType>, /// Optional /// Specifies how the identity platform should return the requested token to your app. /// @@ -55,11 +58,13 @@ pub struct AuthCodeAuthorizationUrl { impl AuthCodeAuthorizationUrl { pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthCodeAuthorizationUrl { + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrl { client_id: client_id.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), authority: Authority::default(), - response_type: vec![ResponseType::Code], + response_type, response_mode: None, nonce: None, state: None, @@ -86,9 +91,88 @@ impl AuthCodeAuthorizationUrl { ) -> AuthorizationResult<Url> { self.authorization_url_with_host(azure_authority_host) } + + pub fn interactive_webview_authentication( + &self, + interactive_web_view_options: Option<InteractiveWebViewOptions>, + ) -> anyhow::Result<AuthResponseQuery> { + let url_string = self + .interactive_authentication(interactive_web_view_options)? + .ok_or(anyhow::Error::msg( + "Unable to get url from redirect in web view".to_string(), + ))?; + dbg!(&url_string); + /* + + + if let Ok(url) = Url::parse(url_string.as_str()) { + dbg!(&url); + + if let Some(query) = url.query() { + let response_query: AuthResponseQuery = serde_urlencoded::from_str(query)?; + } + + } + + let query: HashMap<String, String> = url.query_pairs().map(|(key, value)| (key.to_string(), value.to_string())) + .collect(); + + let code = query.get("code"); + let id_token = query.get("id_token"); + let access_token = query.get("access_token"); + let state = query.get("state"); + let nonce = query.get("nonce"); + dbg!(&code, &id_token, &access_token, &state, &nonce); + */ + + let url = Url::parse(&url_string)?; + let query = url.query().ok_or(AuthorizationFailure::required_value_msg( + "query", + Some(&format!( + "Url returned on redirect is missing query parameters, url: {url}" + )), + ))?; + + let response_query: AuthResponseQuery = serde_urlencoded::from_str(query)?; + Ok(response_query) + } } -impl InteractiveAuthenticator for AuthCodeAuthorizationUrl {} +mod web_view_authenticator { + use crate::identity::{AuthCodeAuthorizationUrl, AuthorizationUrl}; + use crate::web::{InteractiveAuthenticator, InteractiveWebView, InteractiveWebViewOptions}; + + impl InteractiveAuthenticator for AuthCodeAuthorizationUrl { + fn interactive_authentication( + &self, + interactive_web_view_options: Option<InteractiveWebViewOptions>, + ) -> anyhow::Result<Option<String>> { + let url = self.authorization_url()?; + let redirect_url = self.redirect_uri()?; + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let _timeout = web_view_options.timeout; + let (sender, receiver) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + InteractiveWebView::interactive_authentication( + url, + redirect_url, + web_view_options, + sender, + ) + .unwrap(); + }); + + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + while next.is_none() { + next = iter.next(); + } + + Ok(next) + } + } +} impl AuthorizationUrl for AuthCodeAuthorizationUrl { fn redirect_uri(&self) -> AuthorizationResult<Url> { @@ -103,7 +187,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { &self, azure_authority_host: &AzureAuthorityHost, ) -> AuthorizationResult<Url> { - let mut serializer = OAuth::new(); + let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::required_value_msg_result("redirect_uri", None); @@ -188,24 +272,24 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { } let authorization_credentials = vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ResponseType), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::NotRequired(OAuthCredential::ResponseMode), - FormCredential::NotRequired(OAuthCredential::State), - FormCredential::NotRequired(OAuthCredential::Prompt), - FormCredential::NotRequired(OAuthCredential::LoginHint), - FormCredential::NotRequired(OAuthCredential::DomainHint), - FormCredential::NotRequired(OAuthCredential::Nonce), - FormCredential::NotRequired(OAuthCredential::CodeChallenge), - FormCredential::NotRequired(OAuthCredential::CodeChallengeMethod), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ResponseType), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::NotRequired(OAuthParameter::ResponseMode), + SerializerField::NotRequired(OAuthParameter::State), + SerializerField::NotRequired(OAuthParameter::Prompt), + SerializerField::NotRequired(OAuthParameter::LoginHint), + SerializerField::NotRequired(OAuthParameter::DomainHint), + SerializerField::NotRequired(OAuthParameter::Nonce), + SerializerField::NotRequired(OAuthParameter::CodeChallenge), + SerializerField::NotRequired(OAuthParameter::CodeChallengeMethod), ]; let mut encoder = Serializer::new(String::new()); serializer.url_query_encode(authorization_credentials, &mut encoder)?; - if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) @@ -220,7 +304,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { #[derive(Clone)] pub struct AuthCodeAuthorizationUrlBuilder { - authorization_code_authorize_url: AuthCodeAuthorizationUrl, + auth_url_parameters: AuthCodeAuthorizationUrl, } impl Default for AuthCodeAuthorizationUrlBuilder { @@ -231,13 +315,15 @@ impl Default for AuthCodeAuthorizationUrlBuilder { impl AuthCodeAuthorizationUrlBuilder { pub fn new() -> AuthCodeAuthorizationUrlBuilder { + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlBuilder { - authorization_code_authorize_url: AuthCodeAuthorizationUrl { + auth_url_parameters: AuthCodeAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), authority: Authority::default(), response_mode: None, - response_type: vec![ResponseType::Code], + response_type, nonce: None, state: None, scope: vec![], @@ -251,24 +337,23 @@ impl AuthCodeAuthorizationUrlBuilder { } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.authorization_code_authorize_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self.auth_url_parameters.redirect_uri = redirect_uri.as_ref().to_owned(); self } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.authorization_code_authorize_url.client_id = client_id.as_ref().to_owned(); + self.auth_url_parameters.client_id = client_id.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.authorization_code_authorize_url.authority = - Authority::TenantId(tenant.as_ref().to_owned()); + self.auth_url_parameters.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.authorization_code_authorize_url.authority = authority.into(); + self.auth_url_parameters.authority = authority.into(); self } @@ -278,7 +363,7 @@ impl AuthCodeAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.authorization_code_authorize_url.response_type = response_type.into_iter().collect(); + self.auth_url_parameters.response_type = response_type.into_iter().collect(); self } @@ -294,7 +379,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.authorization_code_authorize_url.response_mode = Some(response_mode); + self.auth_url_parameters.response_mode = Some(response_mode); self } @@ -303,7 +388,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.authorization_code_authorize_url.nonce = Some(nonce.as_ref().to_owned()); + self.auth_url_parameters.nonce = Some(nonce.as_ref().to_owned()); self } @@ -319,37 +404,66 @@ impl AuthCodeAuthorizationUrlBuilder { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - let mut buf = [0; 32]; - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; - let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); - - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(base_64_random_string.as_bytes()); - - let nonce = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - self.authorization_code_authorize_url.nonce = Some(nonce); + self.auth_url_parameters.nonce = Some(Crypto::secure_random_string()?); Ok(self) } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.authorization_code_authorize_url.state = Some(state.as_ref().to_owned()); + self.auth_url_parameters.state = Some(state.as_ref().to_owned()); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.authorization_code_authorize_url.scope = - scopes.into_iter().map(|s| s.to_string()).collect(); + self.auth_url_parameters.scope = scopes.into_iter().map(|s| s.to_string()).collect(); + + if self.auth_url_parameters.nonce.is_none() + && self + .auth_url_parameters + .scope + .contains(&String::from("id_token")) + { + let _ = self.with_id_token_scope(); + } self } - /// Automatically adds profile, email, id_token, and offline_access to the scope parameter. + /// Automatically adds `profile` and `email` to the scope parameter. + /// + /// If you need a refresh token then include `offline_access` as a scope. + /// The `offline_access` scope is not included here. pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { + self.auth_url_parameters + .scope + .extend(vec!["profile".to_owned(), "email".to_owned()]); + Ok(self) + } + + /// Adds the `offline_access` scope parameter which tells the authorization server + /// to include a refresh token in the redirect uri query. + pub fn with_refresh_token_scope(&mut self) -> &mut Self { + self.auth_url_parameters + .scope + .extend(vec!["offline_access".to_owned()]); + self + } + + /// Adds the `id_token` scope parameter which tells the authorization server + /// to include an id token in the redirect uri query. + /// + /// Including the `id_token` scope also adds the id_token response type + /// and adds the `openid` scope parameter. + /// + /// Including `id_token` also requires a nonce parameter. + /// This is generated automatically. + /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] + pub fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; - self.with_response_mode(ResponseMode::FormPost); - self.with_response_type(vec![ResponseType::Code, ResponseType::IdToken]); - self.with_scope(vec!["profile", "email", "id_token", "offline_access"]); + self.auth_url_parameters + .response_type + .extend(ResponseType::IdToken); + self.auth_url_parameters + .scope + .extend(vec!["id_token".to_owned()]); Ok(self) } @@ -364,25 +478,24 @@ impl AuthCodeAuthorizationUrlBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.authorization_code_authorize_url.prompt = Some(prompt); + self.auth_url_parameters.prompt = Some(prompt); self } pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.authorization_code_authorize_url.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.auth_url_parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); self } pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.authorization_code_authorize_url.login_hint = Some(login_hint.as_ref().to_owned()); + self.auth_url_parameters.login_hint = Some(login_hint.as_ref().to_owned()); self } /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). /// Required if code_challenge_method is included. pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { - self.authorization_code_authorize_url.code_challenge = - Some(code_challenge.as_ref().to_owned()); + self.auth_url_parameters.code_challenge = Some(code_challenge.as_ref().to_owned()); self } @@ -395,7 +508,7 @@ impl AuthCodeAuthorizationUrlBuilder { &mut self, code_challenge_method: T, ) -> &mut Self { - self.authorization_code_authorize_url.code_challenge_method = + self.auth_url_parameters.code_challenge_method = Some(code_challenge_method.as_ref().to_owned()); self } @@ -413,11 +526,11 @@ impl AuthCodeAuthorizationUrlBuilder { } pub fn build(&self) -> AuthCodeAuthorizationUrl { - self.authorization_code_authorize_url.clone() + self.auth_url_parameters.clone() } pub fn url(&self) -> AuthorizationResult<Url> { - self.authorization_code_authorize_url.url() + self.auth_url_parameters.url() } } @@ -485,7 +598,7 @@ mod test { .with_client_id("client_id") .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) - .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_response_type(vec![ResponseType::IdToken, ResponseType::Code]) .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 01de398c..f1fbf4c8 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, @@ -54,7 +54,7 @@ pub struct AuthorizationCodeCertificateCredential { /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl AuthorizationCodeCertificateCredential { @@ -77,7 +77,7 @@ impl AuthorizationCodeCertificateCredential { scope: vec![], authority: Default::default(), token_credential_options: TokenCredentialOptions::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), } } @@ -102,7 +102,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) @@ -113,8 +113,8 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Authorization code and refresh token cannot be set at the same time - choose one or the other"), ); @@ -122,14 +122,14 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::ClientId.alias(), + OAuthParameter::ClientId.alias(), None, ); } if self.client_assertion.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::ClientAssertion.alias(), + OAuthParameter::ClientAssertion.alias(), None, ); } @@ -153,7 +153,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), None, ); } @@ -163,17 +163,17 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { .grant_type("refresh_token"); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::RefreshToken), - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::ClientAssertion), - FormCredential::Required(OAuthCredential::ClientAssertionType), + SerializerField::Required(OAuthParameter::RefreshToken), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::ClientAssertion), + SerializerField::Required(OAuthParameter::ClientAssertionType), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::AuthorizationCode.alias(), + OAuthParameter::AuthorizationCode.alias(), Some("refresh_token is set but is empty"), ); } @@ -183,22 +183,22 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { .grant_type("authorization_code"); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::AuthorizationCode), - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), - FormCredential::NotRequired(OAuthCredential::CodeVerifier), - FormCredential::Required(OAuthCredential::ClientAssertion), - FormCredential::Required(OAuthCredential::ClientAssertionType), + SerializerField::Required(OAuthParameter::AuthorizationCode), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::NotRequired(OAuthParameter::CodeVerifier), + SerializerField::Required(OAuthParameter::ClientAssertion), + SerializerField::Required(OAuthParameter::ClientAssertionType), ]); } AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Either authorization code or refresh token is required"), ) @@ -224,7 +224,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { scope: vec![], authority: Default::default(), token_credential_options: TokenCredentialOptions::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), }, } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 4b765d11..708ac4a3 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, @@ -55,7 +55,7 @@ pub struct AuthorizationCodeCredential { /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl AuthorizationCodeCredential { @@ -75,7 +75,7 @@ impl AuthorizationCodeCredential { authority: Default::default(), code_verifier: None, token_credential_options: TokenCredentialOptions::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), } } @@ -106,7 +106,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg( "access_token_url", Some("Internal Error"), @@ -114,13 +114,12 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { - let uri = self - .serializer - .get(OAuthCredential::RefreshTokenUrl) - .ok_or(AuthorizationFailure::required_value_msg( + let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( "refresh_token_url", Some("Internal Error"), - ))?; + ), + )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } @@ -130,20 +129,20 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Authorization code and refresh token should not be set at the same time - Internal Error"), ); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { return AuthorizationFailure::required_value_result( - OAuthCredential::ClientSecret.alias(), + OAuthParameter::ClientSecret.alias(), ); } @@ -155,7 +154,7 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); } @@ -165,22 +164,22 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { .refresh_token(refresh_token.as_ref()); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ClientSecret), - FormCredential::Required(OAuthCredential::RefreshToken), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ClientSecret), + SerializerField::Required(OAuthParameter::RefreshToken), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::AuthorizationCode.alias(), + OAuthParameter::AuthorizationCode.alias(), Some("Either authorization code or refresh token is required"), ); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::RedirectUri); + return AuthorizationFailure::required_value_result(OAuthParameter::RedirectUri); } self.serializer @@ -193,21 +192,21 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ClientSecret), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::AuthorizationCode), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), - FormCredential::NotRequired(OAuthCredential::CodeVerifier), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ClientSecret), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::AuthorizationCode), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::NotRequired(OAuthParameter::CodeVerifier), ]); } AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Either authorization code or refresh token is required"), ) @@ -236,7 +235,7 @@ impl AuthorizationCodeCredentialBuilder { authority: Default::default(), code_verifier: None, token_credential_options: TokenCredentialOptions::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), }, } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index e2406bd4..c8825dd5 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, }; @@ -30,7 +30,7 @@ pub struct ClientCertificateCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl ClientCertificateCredential { @@ -74,7 +74,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg( "access_token_url", Some("Internal Error"), @@ -82,25 +82,24 @@ impl AuthorizationSerializer for ClientCertificateCredential { )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { - let uri = self - .serializer - .get(OAuthCredential::RefreshTokenUrl) - .ok_or(AuthorizationFailure::required_value_msg( + let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( "refresh_token_url", Some("Internal Error"), - ))?; + ), + )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } if self.client_assertion.trim().is_empty() { return AuthorizationFailure::required_value_result( - OAuthCredential::ClientAssertion.alias(), + OAuthParameter::ClientAssertion.alias(), ); } @@ -121,7 +120,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { return if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), Some("refresh_token is set but is empty"), ); } @@ -131,21 +130,21 @@ impl AuthorizationSerializer for ClientCertificateCredential { .grant_type("refresh_token"); self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::RefreshToken), - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::ClientAssertion), - FormCredential::Required(OAuthCredential::ClientAssertionType), + SerializerField::Required(OAuthParameter::RefreshToken), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::ClientAssertion), + SerializerField::Required(OAuthParameter::ClientAssertionType), ]) } else { self.serializer.grant_type("client_credentials"); self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::ClientAssertion), - FormCredential::Required(OAuthCredential::ClientAssertionType), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::ClientAssertion), + SerializerField::Required(OAuthParameter::ClientAssertionType), ]) }; } @@ -166,7 +165,7 @@ impl ClientCertificateCredentialBuilder { client_assertion: String::new(), refresh_token: None, token_credential_options: TokenCredentialOptions::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), }, } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 7d3de731..f3ff5d2b 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,4 +1,4 @@ -use crate::auth::{OAuth, OAuthCredential}; +use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{Authority, AzureAuthorityHost}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -35,14 +35,14 @@ impl ClientCredentialsAuthorizationUrl { &self, azure_authority_host: &AzureAuthorityHost, ) -> AuthorizationResult<Url> { - let mut serializer = OAuth::new(); + let mut serializer = OAuthSerializer::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::required_value_result( - OAuthCredential::RedirectUri.alias(), + OAuthParameter::RedirectUri.alias(), ); } @@ -59,23 +59,23 @@ impl ClientCredentialsAuthorizationUrl { let mut encoder = Serializer::new(String::new()); serializer.form_encode_credentials( vec![ - OAuthCredential::ClientId, - OAuthCredential::RedirectUri, - OAuthCredential::State, + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::State, ], &mut encoder, ); let mut url = Url::parse( serializer - .get(OAuthCredential::AuthorizationUrl) + .get(OAuthParameter::AuthorizationUrl) .ok_or(AuthorizationFailure::required_value( - OAuthCredential::AuthorizationUrl.alias(), + OAuthParameter::AuthorizationUrl.alias(), ))? .as_str(), ) .or(AuthorizationFailure::required_value_result( - OAuthCredential::AuthorizationUrl.alias(), + OAuthParameter::AuthorizationUrl.alias(), ))?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 6f4f9b5c..3692cb82 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, ClientCredentialsAuthorizationUrlBuilder, TokenRequest, @@ -42,7 +42,7 @@ pub struct ClientSecretCredential { pub(crate) scopes: Vec<String>, pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl ClientSecretCredential { @@ -53,7 +53,7 @@ impl ClientSecretCredential { scopes: vec![], authority: Default::default(), token_credential_options: Default::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), } } @@ -68,7 +68,7 @@ impl ClientSecretCredential { scopes: vec![], authority: Authority::TenantId(tenant_id.as_ref().to_owned()), token_credential_options: Default::default(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), } } @@ -92,7 +92,7 @@ impl AuthorizationSerializer for ClientSecretCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) @@ -100,11 +100,11 @@ impl AuthorizationSerializer for ClientSecretCredential { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientSecret); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientSecret); } self.serializer.grant_type("client_credentials"); @@ -117,8 +117,8 @@ impl AuthorizationSerializer for ClientSecretCredential { } self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), ]) } diff --git a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs index c36a1014..f47aa2aa 100644 --- a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::oauth::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::oauth::form_credential::SerializerField; use crate::oauth::ResponseType; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -48,7 +48,7 @@ impl CodeFlowAuthorizationUrl { } pub fn url(&self) -> AuthorizationResult<Url> { - let mut serializer = OAuth::new(); + let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::required_value_msg_result("redirect_uri", None); } @@ -71,15 +71,15 @@ impl CodeFlowAuthorizationUrl { let mut encoder = Serializer::new(String::new()); serializer.url_query_encode( vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::ResponseType), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::ResponseType), ], &mut encoder, )?; - if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs index aea51c53..c7f49afa 100644 --- a/graph-oauth/src/identity/credentials/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -34,7 +34,7 @@ pub struct CodeFlowCredential { pub(crate) client_secret: String, /// The same redirect_uri value that was used to acquire the authorization_code. pub(crate) redirect_uri: String, - serializer: OAuth, + serializer: OAuthSerializer, } impl CodeFlowCredential { @@ -50,7 +50,7 @@ impl CodeFlowCredential { client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), - serializer: OAuth::new(), + serializer: OAuthSerializer::new(), } } @@ -72,7 +72,7 @@ impl AuthorizationSerializer for CodeFlowCredential { .authority(azure_authority_host, &Authority::Common); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg( "access_token_url", Some("Internal Error"), @@ -80,13 +80,12 @@ impl AuthorizationSerializer for CodeFlowCredential { )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { - let uri = self - .serializer - .get(OAuthCredential::RefreshTokenUrl) - .ok_or(AuthorizationFailure::required_value_msg( + let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( "refresh_token_url", Some("Internal Error"), - ))?; + ), + )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } @@ -96,25 +95,25 @@ impl AuthorizationSerializer for CodeFlowCredential { return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Authorization code and refresh token should not be set at the same time - Internal Error"), ); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { return AuthorizationFailure::required_value_result( - OAuthCredential::ClientSecret.alias(), + OAuthParameter::ClientSecret.alias(), ); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::RedirectUri); + return AuthorizationFailure::required_value_result(OAuthParameter::RedirectUri); } self.serializer @@ -126,7 +125,7 @@ impl AuthorizationSerializer for CodeFlowCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); } @@ -134,15 +133,15 @@ impl AuthorizationSerializer for CodeFlowCredential { self.serializer.refresh_token(refresh_token.as_ref()); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ClientSecret), - FormCredential::Required(OAuthCredential::RefreshToken), - FormCredential::Required(OAuthCredential::RedirectUri), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ClientSecret), + SerializerField::Required(OAuthParameter::RefreshToken), + SerializerField::Required(OAuthParameter::RedirectUri), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), Some("Either authorization code or refresh token is required"), ); } @@ -151,18 +150,18 @@ impl AuthorizationSerializer for CodeFlowCredential { .authorization_code(authorization_code.as_ref()); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ClientSecret), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::AuthorizationCode), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ClientSecret), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::AuthorizationCode), ]); } AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::AuthorizationCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Either authorization code or refresh token is required"), ) diff --git a/graph-oauth/src/identity/credentials/crypto.rs b/graph-oauth/src/identity/credentials/crypto.rs new file mode 100644 index 00000000..590cf432 --- /dev/null +++ b/graph-oauth/src/identity/credentials/crypto.rs @@ -0,0 +1,23 @@ +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use ring::rand::SecureRandom; + +pub struct Crypto; + +impl Crypto { + pub fn secure_random_string() -> anyhow::Result<String> { + let mut buf = [0; 32]; + + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + + let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); + + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(base_64_random_string.as_bytes()); + + let secure_random_string = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + Ok(secure_random_string) + } +} diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 716b33e3..985776f3 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,8 +1,8 @@ -use crate::auth::{OAuth, OAuthCredential}; +use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; -use crate::oauth::form_credential::FormCredential; +use crate::oauth::form_credential::SerializerField; use crate::oauth::DeviceCode; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -39,7 +39,7 @@ pub struct DeviceAuthorizationCredential { /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl DeviceAuthorizationCredential { @@ -70,7 +70,7 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg( "access_token_url", Some("Internal Error"), @@ -78,13 +78,12 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { - let uri = self - .serializer - .get(OAuthCredential::RefreshTokenUrl) - .ok_or(AuthorizationFailure::required_value_msg( + let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( + AuthorizationFailure::required_value_msg( "refresh_token_url", Some("Internal Error"), - ))?; + ), + )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } @@ -94,15 +93,15 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { return AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::DeviceCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::DeviceCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Device code and refresh token should not be set at the same time - Internal Error"), ); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } self.serializer @@ -112,7 +111,7 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::RefreshToken.alias(), + OAuthParameter::RefreshToken.alias(), Some("Either device code or refresh token is required - found empty refresh token"), ); } @@ -122,15 +121,15 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { .device_code(refresh_token.as_ref()); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::RefreshToken), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::GrantType), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::RefreshToken), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::GrantType), ]); } else if let Some(device_code) = self.device_code.as_ref() { if device_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( - OAuthCredential::DeviceCode.alias(), + OAuthParameter::DeviceCode.alias(), Some( "Either device code or refresh token is required - found empty device code", ), @@ -142,18 +141,18 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { .device_code(device_code.as_ref()); return self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::DeviceCode), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::GrantType), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::DeviceCode), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::GrantType), ]); } AuthorizationFailure::required_value_msg_result( &format!( "{} or {}", - OAuthCredential::DeviceCode.alias(), - OAuthCredential::RefreshToken.alias() + OAuthParameter::DeviceCode.alias(), + OAuthParameter::RefreshToken.alias() ), Some("Either device code or refresh token is required"), ) diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index 62cabfcd..32741218 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{Authority, AzureAuthorityHost, ResponseMode}; use crate::oauth::{Prompt, ResponseType}; use base64::engine::general_purpose::URL_SAFE_NO_PAD; @@ -120,7 +120,7 @@ impl ImplicitCredentialAuthorizationUrl { &self, azure_authority_host: &AzureAuthorityHost, ) -> AuthorizationResult<Url> { - let mut serializer = OAuth::new(); + let mut serializer = OAuthSerializer::new(); if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_result("client_id"); @@ -200,22 +200,22 @@ impl ImplicitCredentialAuthorizationUrl { } let authorization_credentials = vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::ResponseType), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::Nonce), - FormCredential::NotRequired(OAuthCredential::RedirectUri), - FormCredential::NotRequired(OAuthCredential::ResponseMode), - FormCredential::NotRequired(OAuthCredential::State), - FormCredential::NotRequired(OAuthCredential::Prompt), - FormCredential::NotRequired(OAuthCredential::LoginHint), - FormCredential::NotRequired(OAuthCredential::DomainHint), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::ResponseType), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::Nonce), + SerializerField::NotRequired(OAuthParameter::RedirectUri), + SerializerField::NotRequired(OAuthParameter::ResponseMode), + SerializerField::NotRequired(OAuthParameter::State), + SerializerField::NotRequired(OAuthParameter::Prompt), + SerializerField::NotRequired(OAuthParameter::LoginHint), + SerializerField::NotRequired(OAuthParameter::DomainHint), ]; let mut encoder = Serializer::new(String::new()); serializer.url_query_encode(authorization_credentials, &mut encoder)?; - if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 22995c31..a4d8041a 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -7,10 +7,13 @@ mod client_secret_credential; mod code_flow_authorization_url; mod code_flow_credential; mod confidential_client_application; +mod crypto; mod device_code_credential; mod display; mod environment_credential; mod implicit_credential_authorization_url; +mod open_id_authorization_url; +mod open_id_credential; mod prompt; mod proof_key_for_code_exchange; mod public_client_application; @@ -34,10 +37,13 @@ pub use client_secret_credential::*; pub use code_flow_authorization_url::*; pub use code_flow_credential::*; pub use confidential_client_application::*; +pub use crypto::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; pub use implicit_credential_authorization_url::*; +pub use open_id_authorization_url::*; +pub use open_id_credential::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; pub use public_client_application::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs new file mode 100644 index 00000000..96d98623 --- /dev/null +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -0,0 +1,299 @@ +use crate::identity::{ + Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType, +}; +use graph_error::{AuthorizationFailure, AuthorizationResult}; +use url::Url; + +/// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional +/// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your +/// OAuth-enabled applications by using a security token called an ID token. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc +#[derive(Clone, Debug)] +pub struct OpenIdAuthorizationUrl { + /// Required + /// The Application (client) ID that the Azure portal – App registrations experience + /// assigned to your app. + pub(crate) client_id: String, + /// Required + /// The redirect URI of your app, where authentication responses can be sent and received + /// by your app. It must exactly match one of the redirect URIs you registered in the portal, + /// except that it must be URL-encoded. If not present, the endpoint will pick one registered + /// redirect_uri at random to send the user back to. + pub(crate) redirect_uri: String, + /// Required + /// Must include code for OpenID Connect sign-in. + pub(crate) response_type: Vec<ResponseType>, + /// Optional + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - query: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter isn't supported when requesting an + /// ID token by using the implicit flow. + /// - fragment: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - form_post: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. + pub(crate) response_mode: Option<ResponseMode>, + /// Optional + /// A value generated and sent by your app in its request for an ID token. The same nonce + /// value is included in the ID token returned to your app by the Microsoft identity platform. + /// To mitigate token replay attacks, your app should verify the nonce value in the ID token + /// is the same value it sent when requesting the token. The value is typically a unique, + /// random string. + pub(crate) nonce: String, + /// Required + /// A value included in the request that also will be returned in the token response. + /// It can be a string of any content you want. A randomly generated unique value typically + /// is used to prevent cross-site request forgery attacks. The state also is used to encode + /// information about the user's state in the app before the authentication request occurred, + /// such as the page or view the user was on. + pub(crate) state: Option<String>, + /// Required - the openid scope is already included + /// A space-separated list of scopes. For OpenID Connect, it must include the scope openid, + /// which translates to the Sign you in permission in the consent UI. You might also include + /// other scopes in this request for requesting consent. + pub(crate) scope: Vec<String>, + /// Optional + /// Indicates the type of user interaction that is required. The only valid values at + /// this time are login, none, consent, and select_account. + /// + /// The [Prompt::Login] claim forces the user to enter their credentials on that request, + /// which negates single sign-on. + /// + /// The [Prompt::None] parameter is the opposite, and should be paired with a login_hint to + /// indicate which user must be signed in. These parameters ensure that the user isn't + /// presented with any interactive prompt at all. If the request can't be completed silently + /// via single sign-on, the Microsoft identity platform returns an error. Causes include no + /// signed-in user, the hinted user isn't signed in, or multiple users are signed in but no + /// hint was provided. + /// + /// The [Prompt::Consent] claim triggers the OAuth consent dialog after the + /// user signs in. The dialog asks the user to grant permissions to the app. + /// + /// Finally, [Prompt::SelectAccount] shows the user an account selector, negating silent SSO but + /// allowing the user to pick which account they intend to sign in with, without requiring + /// credential entry. You can't use both login_hint and select_account. + pub(crate) prompt: Vec<Prompt>, + /// Optional + /// The realm of the user in a federated directory. This skips the email-based discovery + /// process that the user goes through on the sign-in page, for a slightly more streamlined + /// user experience. For tenants that are federated through an on-premises directory + /// like AD FS, this often results in a seamless sign-in because of the existing login session. + pub(crate) domain_hint: Option<String>, + /// Optional + /// You can use this parameter to pre-fill the username and email address field of the + /// sign-in page for the user, if you know the username ahead of time. Often, apps use + /// this parameter during reauthentication, after already extracting the login_hint + /// optional claim from an earlier sign-in. + pub(crate) login_hint: Option<String>, + pub(crate) authority: Authority, +} + +impl OpenIdAuthorizationUrl { + pub fn new<T: AsRef<str>>( + client_id: T, + redirect_uri: T, + ) -> anyhow::Result<OpenIdAuthorizationUrl> { + Ok(OpenIdAuthorizationUrl { + client_id: client_id.as_ref().to_owned(), + redirect_uri: redirect_uri.as_ref().to_owned(), + response_type: vec![ResponseType::Code], + response_mode: None, + nonce: Crypto::secure_random_string()?, + state: None, + scope: vec!["openid".to_owned()], + prompt: vec![], + domain_hint: None, + login_hint: None, + authority: Authority::default(), + }) + } + + pub fn builder() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + OpenIdAuthorizationUrlBuilder::new() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.url_with_host(&AzureAuthorityHost::default()) + } + + pub fn url_with_host( + &self, + azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { + self.authorization_url_with_host(azure_authority_host) + } +} + +impl AuthorizationUrl for OpenIdAuthorizationUrl { + fn redirect_uri(&self) -> AuthorizationResult<Url> { + Url::parse(self.redirect_uri.as_str()).map_err(AuthorizationFailure::from) + } + + fn authorization_url(&self) -> AuthorizationResult<Url> { + self.authorization_url_with_host(&AzureAuthorityHost::default()) + } + + fn authorization_url_with_host( + &self, + _azure_authority_host: &AzureAuthorityHost, + ) -> AuthorizationResult<Url> { + unimplemented!() + } +} + +pub struct OpenIdAuthorizationUrlBuilder { + auth_url_parameters: OpenIdAuthorizationUrl, +} + +impl OpenIdAuthorizationUrlBuilder { + fn new() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + Ok(OpenIdAuthorizationUrlBuilder { + auth_url_parameters: OpenIdAuthorizationUrl::new(String::new(), String::new())?, + }) + } + + pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { + self.auth_url_parameters.redirect_uri = redirect_uri.as_ref().to_owned(); + self + } + + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { + self.auth_url_parameters.client_id = client_id.as_ref().to_owned(); + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { + self.auth_url_parameters.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.auth_url_parameters.authority = authority.into(); + self + } + + /// Default is code. Must include code for the authorization code flow. + /// Can also include id_token or token if using the hybrid flow. + pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( + &mut self, + response_type: I, + ) -> &mut Self { + self.auth_url_parameters.response_type = response_type.into_iter().collect(); + self + } + + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - **query**: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter is not supported when requesting an + /// ID token by using the implicit flow. + /// - **fragment**: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - **form_post**: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. + pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { + self.auth_url_parameters.response_mode = Some(response_mode); + self + } + + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + /// + /// Because openid requires a nonce as part of the OAuth flow a nonce is already included. + /// The nonce is generated internally using the same requirements of generating a secure + /// random string as is done when using proof key for code exchange (PKCE) in the + /// authorization code grant. If you are unsure or unclear how the nonce works then it is + /// recommended to stay with the generated nonce as it is cryptographically secure. + pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { + self.auth_url_parameters.nonce = nonce.as_ref().to_owned(); + self + } + + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + /// + /// The nonce is generated in the same way as generating a PKCE. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + #[doc(hidden)] + fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { + self.auth_url_parameters.nonce = Crypto::secure_random_string()?; + Ok(self) + } + + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { + self.auth_url_parameters.state = Some(state.as_ref().to_owned()); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { + self.auth_url_parameters.scope = scopes.into_iter().map(|s| s.to_string()).collect(); + self + } + + /// Automatically adds profile, email, id_token, and offline_access to the scope parameter. + /// The openid scope is already included when using [OpenIdCredential] + pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { + self.with_nonce_generated()?; + self.with_response_mode(ResponseMode::FormPost); + self.with_response_type(vec![ResponseType::Code, ResponseType::IdToken]); + self.with_scope(vec!["profile", "email", "id_token", "offline_access"]); + Ok(self) + } + + /// Indicates the type of user interaction that is required. Valid values are login, none, + /// consent, and select_account. + /// + /// - **prompt=login** forces the user to enter their credentials on that request, negating single-sign on. + /// - **prompt=none** is the opposite. It ensures that the user isn't presented with any interactive prompt. + /// If the request can't be completed silently by using single-sign on, the Microsoft identity platform returns an interaction_required error. + /// - **prompt=consent** triggers the OAuth consent dialog after the user signs in, asking the user to + /// grant permissions to the app. + /// - **prompt=select_account** interrupts single sign-on providing account selection experience + /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. + pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self { + self.auth_url_parameters.prompt = prompt.into_iter().collect(); + self + } + + /// Optional + /// The realm of the user in a federated directory. This skips the email-based discovery + /// process that the user goes through on the sign-in page, for a slightly more streamlined + /// user experience. For tenants that are federated through an on-premises directory + /// like AD FS, this often results in a seamless sign-in because of the existing login session. + pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { + self.auth_url_parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); + self + } + + /// Optional + /// You can use this parameter to pre-fill the username and email address field of the + /// sign-in page for the user, if you know the username ahead of time. Often, apps use + /// this parameter during reauthentication, after already extracting the login_hint + /// optional claim from an earlier sign-in. + pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { + self.auth_url_parameters.login_hint = Some(login_hint.as_ref().to_owned()); + self + } + + pub fn build(&self) -> OpenIdAuthorizationUrl { + self.auth_url_parameters.clone() + } + + pub fn url(&self) -> AuthorizationResult<Url> { + self.auth_url_parameters.url() + } +} diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs new file mode 100644 index 00000000..ae859e87 --- /dev/null +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -0,0 +1 @@ +pub struct OpenIdCredential {} diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index 72387bd5..bfa70587 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -38,3 +38,12 @@ impl AsRef<str> for Prompt { } } } + +impl IntoIterator for Prompt { + type Item = Prompt; + type IntoIter = std::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + vec![self].into_iter() + } +} diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 3ef939b3..e8eb8fff 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::identity::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; @@ -31,7 +31,7 @@ pub struct ResourceOwnerPasswordCredential { pub(crate) scope: Vec<String>, pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, - serializer: OAuth, + serializer: OAuthSerializer, } impl ResourceOwnerPasswordCredential { @@ -74,7 +74,7 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthCredential::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) @@ -82,15 +82,15 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::ClientId.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } if self.username.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::Username.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::Username.alias()); } if self.password.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthCredential::Password.alias()); + return AuthorizationFailure::required_value_result(OAuthParameter::Password.alias()); } self.serializer @@ -99,9 +99,9 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { .extend_scopes(self.scope.iter()); self.serializer.authorization_form(vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::GrantType), - FormCredential::NotRequired(OAuthCredential::Scope), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::GrantType), + SerializerField::NotRequired(OAuthParameter::Scope), ]) } diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index 8ac947b1..a2ccd145 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ResponseType { #[default] Code, diff --git a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs index a8af1774..21f445cf 100644 --- a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs @@ -1,5 +1,5 @@ -use crate::auth::{OAuth, OAuthCredential}; -use crate::oauth::form_credential::FormCredential; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::oauth::form_credential::SerializerField; use crate::oauth::ResponseType; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -35,7 +35,7 @@ impl TokenFlowAuthorizationUrl { } pub fn url(&self) -> AuthorizationResult<Url> { - let mut serializer = OAuth::new(); + let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::required_value_msg_result("redirect_uri", None); } @@ -58,15 +58,15 @@ impl TokenFlowAuthorizationUrl { let mut encoder = Serializer::new(String::new()); serializer.url_query_encode( vec![ - FormCredential::Required(OAuthCredential::ClientId), - FormCredential::Required(OAuthCredential::RedirectUri), - FormCredential::Required(OAuthCredential::Scope), - FormCredential::Required(OAuthCredential::ResponseType), + SerializerField::Required(OAuthParameter::ClientId), + SerializerField::Required(OAuthParameter::RedirectUri), + SerializerField::Required(OAuthParameter::Scope), + SerializerField::Required(OAuthParameter::ResponseType), ], &mut encoder, )?; - if let Some(authorization_url) = serializer.get(OAuthCredential::AuthorizationUrl) { + if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/form_credential.rs b/graph-oauth/src/identity/form_credential.rs index 75a943a1..184a3edb 100644 --- a/graph-oauth/src/identity/form_credential.rs +++ b/graph-oauth/src/identity/form_credential.rs @@ -1,7 +1,7 @@ -use crate::auth::OAuthCredential; +use crate::auth::OAuthParameter; #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum FormCredential { - Required(OAuthCredential), - NotRequired(OAuthCredential), +pub enum SerializerField { + Required(OAuthParameter), + NotRequired(OAuthParameter), } diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index a1f6f039..58278d87 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -63,6 +63,7 @@ extern crate pretty_env_logger; mod access_token; mod auth; +mod auth_response_query; mod device_code; mod discovery; mod grants; @@ -76,8 +77,9 @@ pub mod web; pub mod oauth { pub use crate::access_token::AccessToken; pub use crate::auth::GrantSelector; - pub use crate::auth::OAuth; - pub use crate::auth::OAuthCredential; + pub use crate::auth::OAuthParameter; + pub use crate::auth::OAuthSerializer; + pub use crate::auth_response_query::*; pub use crate::device_code::*; pub use crate::discovery::graph_discovery; pub use crate::discovery::jwt_keys; diff --git a/graph-oauth/src/oauth_error.rs b/graph-oauth/src/oauth_error.rs index 7cd80ec8..55414035 100644 --- a/graph-oauth/src/oauth_error.rs +++ b/graph-oauth/src/oauth_error.rs @@ -1,4 +1,4 @@ -use crate::auth::OAuthCredential; +use crate::auth::OAuthParameter; use crate::grants::{GrantRequest, GrantType}; use graph_error::{GraphFailure, GraphResult}; use std::error; @@ -26,11 +26,11 @@ impl OAuthError { OAuthError::error_kind(ErrorKind::InvalidData, msg) } - pub fn error_from<T>(c: OAuthCredential) -> Result<T, GraphFailure> { + pub fn error_from<T>(c: OAuthParameter) -> Result<T, GraphFailure> { Err(OAuthError::credential_error(c)) } - pub fn credential_error(c: OAuthCredential) -> GraphFailure { + pub fn credential_error(c: OAuthParameter) -> GraphFailure { GraphFailure::error_kind( ErrorKind::NotFound, format!("MISSING OR INVALID: {c:#?}").as_str(), diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index abaf41c7..88f76c47 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -1,18 +1,32 @@ use crate::identity::AuthorizationUrl; -use crate::web::{InteractiveWebView, InteractiveWebViewOptions}; +use crate::web::InteractiveWebViewOptions; pub trait InteractiveAuthenticator: AuthorizationUrl { fn interactive_authentication( &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, - ) -> anyhow::Result<()> { - let url = self.authorization_url()?; + ) -> anyhow::Result<Option<String>>; +} + +/* +let url = self.authorization_url()?; let redirect_url = self.redirect_uri()?; + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let timeout = web_view_options.timeout.clone(); + let (sender, receiver) = std::sync::mpsc::channel(); + + let handle = std::thread::spawn(move || { + match receiver.recv_timeout(timeout) { + Ok(url) => return Ok(url), + Err(e) => Err(e) + } + }); + InteractiveWebView::interactive_authentication( &url, &redirect_url, - interactive_web_view_options.unwrap_or_default(), + web_view_options, + sender )?; - Ok(()) - } -} + handle.join().unwrap().map_err(anyhow::Error::from) + */ diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 347b05ce..217d52ba 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,6 +1,9 @@ +use anyhow::Context; +use std::time::Duration; use url::Url; use crate::web::InteractiveWebViewOptions; +use wry::application::platform::windows::EventLoopExtWindows; use wry::{ application::{ event::{Event, StartCause, WindowEvent}, @@ -58,14 +61,16 @@ pub struct InteractiveWebView; impl InteractiveWebView { pub fn interactive_authentication( - uri: &Url, - redirect_uri: &Url, + uri: Url, + redirect_uri: Url, options: InteractiveWebViewOptions, + sender: std::sync::mpsc::Sender<String>, ) -> anyhow::Result<()> { - let event_loop: EventLoop<UserEvents> = EventLoop::<UserEvents>::with_user_event(); + let event_loop: EventLoop<UserEvents> = EventLoop::<UserEvents>::new_any_thread(); let proxy = event_loop.create_proxy(); + let sender2 = sender.clone(); - let validator = WebViewValidHosts::new(uri.clone(), redirect_uri.clone())?; + let validator = WebViewValidHosts::new(uri.clone(), redirect_uri)?; let window = WindowBuilder::new() .with_title("Sign In") @@ -87,6 +92,8 @@ impl InteractiveWebView { let is_redirect = validator.is_redirect_host(&url); if is_redirect { + sender2.send(uri.clone()).context("mpsc error").unwrap(); + std::thread::sleep(Duration::from_secs(1)); let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); return true; } @@ -107,26 +114,26 @@ impl InteractiveWebView { *control_flow = ControlFlow::Wait; match event { - Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::NewEvents(StartCause::Init) => info!("Webview runtime started"), Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { - let _ = webview.clear_all_browsing_data(); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { dbg!(&uri); - let _ = webview.clear_all_browsing_data(); + info!("Matched on redirect uri - closing window: {uri:#?}"); + sender.send(uri.to_string()).unwrap(); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::InvalidNavigationAttempt(url_option)) => { - error!("WebView or possible attacker attempted to navigate to invalid host - closing window for security reasons. Possible url attempted: {url_option:#?}"); + error!("WebView attempted to navigate to invalid host - closing window for security reasons. Possible url attempted: {url_option:#?}"); let _ = webview.clear_all_browsing_data(); *control_flow = ControlFlow::Exit; if options.panic_on_invalid_uri_navigation_attempt { - panic!("WebView or possible attacker attempted to navigate to invalid host. Possible url attempted: {url_option:#?}") + panic!("WebView attempted to navigate to invalid host. Possible url attempted: {url_option:#?}") } } _ => (), diff --git a/graph-oauth/src/web/interactive_web_view_options.rs b/graph-oauth/src/web/interactive_web_view_options.rs index 261fa79d..c87c6f70 100644 --- a/graph-oauth/src/web/interactive_web_view_options.rs +++ b/graph-oauth/src/web/interactive_web_view_options.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + #[derive(Clone)] pub struct InteractiveWebViewOptions { pub panic_on_invalid_uri_navigation_attempt: bool, @@ -6,6 +8,7 @@ pub struct InteractiveWebViewOptions { /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. pub ports: Vec<usize>, + pub timeout: Duration, } impl Default for InteractiveWebViewOptions { @@ -14,6 +17,8 @@ impl Default for InteractiveWebViewOptions { panic_on_invalid_uri_navigation_attempt: true, theme: None, ports: vec![], + // 10 Minutes default timeout + timeout: Duration::from_secs(10 * 60), } } } diff --git a/src/client/graph.rs b/src/client/graph.rs index 9c34b7bc..cd69f0cc 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AccessToken, AllowedHostValidator, HostValidator, OAuth}; +use crate::oauth::{AccessToken, AllowedHostValidator, HostValidator, OAuthSerializer}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -532,10 +532,10 @@ impl From<&AccessToken> for Graph { } } -impl TryFrom<&OAuth> for Graph { +impl TryFrom<&OAuthSerializer> for Graph { type Error = GraphFailure; - fn try_from(oauth: &OAuth) -> Result<Self, Self::Error> { + fn try_from(oauth: &OAuthSerializer) -> Result<Self, Self::Error> { let access_token = oauth .get_access_token() .ok_or_else(|| GraphFailure::not_found("no access token"))?; diff --git a/src/lib.rs b/src/lib.rs index 690637dc..3f4f6435 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,8 +179,8 @@ //! //! # Example //! ``` -//! use graph_rs_sdk::oauth::OAuth; -//! let mut oauth = OAuth::new(); +//! use graph_rs_sdk::oauth::OAuthSerializer; +//! let mut oauth = OAuthSerializer::new(); //! oauth //! .client_id("<YOUR_CLIENT_ID>") //! .client_secret("<YOUR_CLIENT_SECRET>") diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs index baf4e69f..faeb1e3e 100644 --- a/test-tools/src/oauth.rs +++ b/test-tools/src/oauth.rs @@ -6,29 +6,29 @@ use url::Url; pub struct OAuthTestTool; impl OAuthTestTool { - fn match_grant_credential(grant_request: GrantRequest) -> OAuthCredential { + fn match_grant_credential(grant_request: GrantRequest) -> OAuthParameter { match grant_request { - GrantRequest::Authorization => OAuthCredential::AuthorizationUrl, - GrantRequest::AccessToken => OAuthCredential::AccessTokenUrl, - GrantRequest::RefreshToken => OAuthCredential::RefreshTokenUrl, + GrantRequest::Authorization => OAuthParameter::AuthorizationUrl, + GrantRequest::AccessToken => OAuthParameter::AccessTokenUrl, + GrantRequest::RefreshToken => OAuthParameter::RefreshTokenUrl, } } pub fn oauth_query_uri_test( - oauth: &mut OAuth, + oauth: &mut OAuthSerializer, grant_type: GrantType, grant_request: GrantRequest, - includes: Vec<OAuthCredential>, + includes: Vec<OAuthParameter>, ) { let mut url = String::new(); if grant_request.eq(&GrantRequest::AccessToken) { - let mut atu = oauth.get(OAuthCredential::AccessTokenUrl).unwrap(); + let mut atu = oauth.get(OAuthParameter::AccessTokenUrl).unwrap(); if !atu.ends_with('?') { atu.push('?'); } url.push_str(atu.as_str()); } else if grant_request.eq(&GrantRequest::RefreshToken) { - let mut rtu = oauth.get(OAuthCredential::RefreshTokenUrl).unwrap(); + let mut rtu = oauth.get(OAuthParameter::RefreshTokenUrl).unwrap(); if !rtu.ends_with('?') { rtu.push('?'); } @@ -45,9 +45,9 @@ impl OAuthTestTool { let mut cow_cred_false: Vec<(Cow<str>, Cow<str>)> = Vec::new(); let not_includes = OAuthTestTool::credentials_not_including(&includes); - for oac in OAuthCredential::iter() { + for oac in OAuthParameter::iter() { if oauth.contains(oac) && includes.contains(&oac) && !not_includes.contains(&oac) { - if oac.eq(&OAuthCredential::Scope) { + if oac.eq(&OAuthParameter::Scope) { let s = oauth.join_scopes(" "); cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { @@ -55,7 +55,7 @@ impl OAuthTestTool { cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } } else if oauth.contains(oac) && not_includes.contains(&oac) { - if oac.eq(&OAuthCredential::Scope) { + if oac.eq(&OAuthParameter::Scope) { let s = oauth.join_scopes(" "); cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { @@ -74,9 +74,9 @@ impl OAuthTestTool { } } - fn credentials_not_including(included: &[OAuthCredential]) -> Vec<OAuthCredential> { + fn credentials_not_including(included: &[OAuthParameter]) -> Vec<OAuthParameter> { let mut vec = Vec::new(); - for oac in OAuthCredential::iter() { + for oac in OAuthParameter::iter() { if !included.contains(&oac) { vec.push(oac); } @@ -85,7 +85,7 @@ impl OAuthTestTool { vec } - pub fn oauth_contains_credentials(oauth: &mut OAuth, credentials: &[OAuthCredential]) { + pub fn oauth_contains_credentials(oauth: &mut OAuthSerializer, credentials: &[OAuthParameter]) { for oac in credentials.iter() { assert!(oauth.contains(*oac)); } @@ -102,31 +102,31 @@ impl OAuthTestTool { pub fn for_each_fn_scope<F>(mut func: F, scopes: &[String]) where - F: FnMut(&mut OAuth, &[String]), + F: FnMut(&mut OAuthSerializer, &[String]), { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth.extend_scopes(scopes); func(&mut oauth, scopes) } - pub fn join_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn join_scopes(oauth: &mut OAuthSerializer, s: &[String]) { assert_eq!(s.join(" "), oauth.join_scopes(" ")); } - pub fn contains_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn contains_scopes(oauth: &mut OAuthSerializer, s: &[String]) { for string in s { assert!(oauth.contains_scope(string.as_str())); } } - pub fn remove_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn remove_scopes(oauth: &mut OAuthSerializer, s: &[String]) { for string in s { oauth.remove_scope(string.as_str()); assert!(!oauth.contains_scope(string)); } } - pub fn get_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn get_scopes(oauth: &mut OAuthSerializer, s: &[String]) { assert_eq!( s, oauth @@ -138,14 +138,14 @@ impl OAuthTestTool { ) } - pub fn clear_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn clear_scopes(oauth: &mut OAuthSerializer, s: &[String]) { OAuthTestTool::join_scopes(oauth, s); assert!(!oauth.get_scopes().is_empty()); oauth.clear_scopes(); assert!(oauth.get_scopes().is_empty()) } - pub fn distinct_scopes(oauth: &mut OAuth, s: &[String]) { + pub fn distinct_scopes(oauth: &mut OAuthSerializer, s: &[String]) { assert_eq!(s.len(), oauth.get_scopes().len()); let s0 = &s[0]; oauth.add_scope(s0.as_str()); diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index ca884fbe..1739ffd4 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -2,7 +2,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; -use graph_rs_sdk::oauth::{AccessToken, ClientSecretCredential, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, ClientSecretCredential, OAuthSerializer}; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -131,8 +131,8 @@ impl OAuthTestCredentials { creds } - fn into_oauth(self) -> OAuth { - let mut oauth = OAuth::new(); + fn into_oauth(self) -> OAuthSerializer { + let mut oauth = OAuthSerializer::new(); oauth .client_id(self.client_id.as_str()) .client_secret(self.client_secret.as_str()) @@ -164,7 +164,7 @@ pub enum OAuthTestClient { impl OAuthTestClient { fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, AccessToken)> { let user_id = creds.user_id.clone()?; - let mut oauth: OAuth = creds.into_oauth(); + let mut oauth: OAuthSerializer = creds.into_oauth(); let mut req = { match self { OAuthTestClient::ClientCredentials => oauth.build().client_credentials(), @@ -190,7 +190,7 @@ impl OAuthTestClient { creds: OAuthTestCredentials, ) -> Option<(String, AccessToken)> { let user_id = creds.user_id.clone()?; - let mut oauth: OAuth = creds.into_oauth(); + let mut oauth: OAuthSerializer = creds.into_oauth(); let mut req = { match self { OAuthTestClient::ClientCredentials => oauth.build_async().client_credentials(), diff --git a/tests/discovery_tests.rs b/tests/discovery_tests.rs index 8aa7b986..327700f7 100644 --- a/tests/discovery_tests.rs +++ b/tests/discovery_tests.rs @@ -1,71 +1,71 @@ use graph_oauth::oauth::jwt_keys::JWTKeys; -use graph_oauth::oauth::{OAuth, OAuthCredential}; +use graph_oauth::oauth::{OAuthParameter, OAuthSerializer}; use graph_rs_sdk::oauth::graph_discovery::{ GraphDiscovery, MicrosoftSigningKeysV1, MicrosoftSigningKeysV2, }; #[test] fn graph_discovery_oauth_v1() { - let oauth: OAuth = GraphDiscovery::V1.oauth().unwrap(); + let oauth: OAuthSerializer = GraphDiscovery::V1.oauth().unwrap(); let keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.signing_keys().unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizationUrl), + oauth.get(OAuthParameter::AuthorizationUrl), Some(keys.authorization_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenUrl), + oauth.get(OAuthParameter::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenUrl), + oauth.get(OAuthParameter::RefreshTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::LogoutURL), + oauth.get(OAuthParameter::LogoutURL), Some(keys.end_session_endpoint) ); } #[test] fn graph_discovery_oauth_v2() { - let oauth: OAuth = GraphDiscovery::V2.oauth().unwrap(); + let oauth: OAuthSerializer = GraphDiscovery::V2.oauth().unwrap(); let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.signing_keys().unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizationUrl), + oauth.get(OAuthParameter::AuthorizationUrl), Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenUrl), + oauth.get(OAuthParameter::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenUrl), + oauth.get(OAuthParameter::RefreshTokenUrl), Some(keys.token_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::LogoutURL), + oauth.get(OAuthParameter::LogoutURL), Some(keys.end_session_endpoint) ); } #[tokio::test] async fn async_graph_discovery_oauth_v2() { - let oauth: OAuth = GraphDiscovery::V2.async_oauth().await.unwrap(); + let oauth: OAuthSerializer = GraphDiscovery::V2.async_oauth().await.unwrap(); let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.async_signing_keys().await.unwrap(); assert_eq!( - oauth.get(OAuthCredential::AuthorizationUrl), + oauth.get(OAuthParameter::AuthorizationUrl), Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::AccessTokenUrl), + oauth.get(OAuthParameter::AccessTokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthCredential::RefreshTokenUrl), + oauth.get(OAuthParameter::RefreshTokenUrl), Some(keys.token_endpoint) ); assert_eq!( - oauth.get(OAuthCredential::LogoutURL), + oauth.get(OAuthParameter::LogoutURL), Some(keys.end_session_endpoint) ); } diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index 75afa0a5..5a47dd70 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -1,11 +1,11 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuthSerializer}; use test_tools::oauth::OAuthTestTool; use url::{Host, Url}; #[test] pub fn authorization_url() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") @@ -37,7 +37,7 @@ pub fn authorization_url() { #[test] fn access_token_uri() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .client_secret("CLDIE3F") @@ -58,7 +58,7 @@ fn access_token_uri() { #[test] fn refresh_token_uri() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .client_secret("CLDIE3F") @@ -82,7 +82,7 @@ fn refresh_token_uri() { #[test] pub fn access_token_body_contains() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index 9e2e11dc..cc8e1169 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -1,10 +1,10 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuth}; +use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuthSerializer}; #[test] fn sign_in_code_url() { // Test the sign in url with a manually set response type. - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") @@ -23,7 +23,7 @@ fn sign_in_code_url() { #[test] fn sign_in_code_url_with_state() { // Test the sign in url with a manually set response type. - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://example.com/oauth2/v2.0/authorize") .client_id("bb301aaa-1201-4259-a230923fds32") @@ -41,7 +41,7 @@ fn sign_in_code_url_with_state() { #[test] fn access_token() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") @@ -67,7 +67,7 @@ fn access_token() { #[test] fn refresh_token() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") @@ -90,7 +90,7 @@ fn refresh_token() { #[test] fn get_refresh_token() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") @@ -109,7 +109,7 @@ fn get_refresh_token() { #[test] fn multi_scope() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .add_scope("Files.Read") diff --git a/tests/grants_implicit.rs b/tests/grants_implicit.rs index 9f5fa65a..4f4a660d 100644 --- a/tests/grants_implicit.rs +++ b/tests/grants_implicit.rs @@ -1,8 +1,8 @@ -use graph_rs_sdk::oauth::{GrantRequest, GrantType, OAuth}; +use graph_rs_sdk::oauth::{GrantRequest, GrantType, OAuthSerializer}; #[test] pub fn implicit_grant_url() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") diff --git a/tests/grants_openid.rs b/tests/grants_openid.rs index a384ab85..04c7e12b 100644 --- a/tests/grants_openid.rs +++ b/tests/grants_openid.rs @@ -1,9 +1,9 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, OAuth}; +use graph_rs_sdk::oauth::{GrantRequest, OAuthSerializer}; use url::{Host, Url}; -pub fn oauth() -> OAuth { - let mut oauth = OAuth::new(); +pub fn oauth() -> OAuthSerializer { + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") diff --git a/tests/grants_token_flow.rs b/tests/grants_token_flow.rs index 0e864f7d..6f25bf6f 100644 --- a/tests/grants_token_flow.rs +++ b/tests/grants_token_flow.rs @@ -1,9 +1,9 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, OAuth}; +use graph_rs_sdk::oauth::{GrantRequest, OAuthSerializer}; #[test] pub fn token_flow_url() { - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .authorization_url("https://login.live.com/oauth20_authorize.srf?") .client_id("bb301aaa-1201-4259-a230923fds32") diff --git a/tests/oauth_tests.rs b/tests/oauth_tests.rs index 42da1b90..a9313b84 100644 --- a/tests/oauth_tests.rs +++ b/tests/oauth_tests.rs @@ -1,11 +1,11 @@ use graph_oauth::oauth::IntoEnumIterator; -use graph_oauth::oauth::{OAuth, OAuthCredential}; +use graph_oauth::oauth::{OAuthParameter, OAuthSerializer}; #[test] fn oauth_parameters_from_credential() { // Doesn't matter the flow here as this is for testing // that the credentials are entered/retrieved correctly. - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("client_id") .client_secret("client_secret") @@ -30,67 +30,67 @@ fn oauth_parameters_from_credential() { .logout_url("https://example.com/logout?") .post_logout_redirect_uri("https://example.com/redirect?"); - OAuthCredential::iter().for_each(|credential| { + OAuthParameter::iter().for_each(|credential| { if oauth.contains(credential) { match credential { - OAuthCredential::ClientId => { + OAuthParameter::ClientId => { assert_eq!(oauth.get(credential), Some("client_id".into())) } - OAuthCredential::ClientSecret => { + OAuthParameter::ClientSecret => { assert_eq!(oauth.get(credential), Some("client_secret".into())) } - OAuthCredential::AuthorizationUrl => assert_eq!( + OAuthParameter::AuthorizationUrl => assert_eq!( oauth.get(credential), Some("https://example.com/authorize?".into()) ), - OAuthCredential::AccessTokenUrl => assert_eq!( + OAuthParameter::AccessTokenUrl => assert_eq!( oauth.get(credential), Some("https://example.com/token?".into()) ), - OAuthCredential::RefreshTokenUrl => assert_eq!( + OAuthParameter::RefreshTokenUrl => assert_eq!( oauth.get(credential), Some("https://example.com/token?".into()) ), - OAuthCredential::RedirectUri => assert_eq!( + OAuthParameter::RedirectUri => assert_eq!( oauth.get(credential), Some("https://example.com/redirect?".into()) ), - OAuthCredential::AuthorizationCode => { + OAuthParameter::AuthorizationCode => { assert_eq!(oauth.get(credential), Some("ADSLFJL4L3".into())) } - OAuthCredential::ResponseMode => { + OAuthParameter::ResponseMode => { assert_eq!(oauth.get(credential), Some("response_mode".into())) } - OAuthCredential::ResponseType => { + OAuthParameter::ResponseType => { assert_eq!(oauth.get(credential), Some("response_type".into())) } - OAuthCredential::State => assert_eq!(oauth.get(credential), Some("state".into())), - OAuthCredential::GrantType => { + OAuthParameter::State => assert_eq!(oauth.get(credential), Some("state".into())), + OAuthParameter::GrantType => { assert_eq!(oauth.get(credential), Some("grant_type".into())) } - OAuthCredential::Nonce => assert_eq!(oauth.get(credential), Some("nonce".into())), - OAuthCredential::LogoutURL => assert_eq!( + OAuthParameter::Nonce => assert_eq!(oauth.get(credential), Some("nonce".into())), + OAuthParameter::LogoutURL => assert_eq!( oauth.get(credential), Some("https://example.com/logout?".into()) ), - OAuthCredential::PostLogoutRedirectURI => assert_eq!( + OAuthParameter::PostLogoutRedirectURI => assert_eq!( oauth.get(credential), Some("https://example.com/redirect?".into()) ), - OAuthCredential::Prompt => assert_eq!(oauth.get(credential), Some("login".into())), - OAuthCredential::SessionState => { + OAuthParameter::Prompt => assert_eq!(oauth.get(credential), Some("login".into())), + OAuthParameter::SessionState => { assert_eq!(oauth.get(credential), Some("session_state".into())) } - OAuthCredential::ClientAssertion => { + OAuthParameter::ClientAssertion => { assert_eq!(oauth.get(credential), Some("client_assertion".into())) } - OAuthCredential::ClientAssertionType => { + OAuthParameter::ClientAssertionType => { assert_eq!(oauth.get(credential), Some("client_assertion_type".into())) } - OAuthCredential::CodeVerifier => { + OAuthParameter::CodeVerifier => { assert_eq!(oauth.get(credential), Some("code_verifier".into())) } - OAuthCredential::Resource => { + OAuthParameter::Resource => { assert_eq!(oauth.get(credential), Some("resource".into())) } _ => {} @@ -103,7 +103,7 @@ fn oauth_parameters_from_credential() { fn remove_credential() { // Doesn't matter the flow here as this is for testing // that the credentials are entered/retrieved correctly. - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") @@ -111,22 +111,22 @@ fn remove_credential() { .authorization_url("https://www.example.com/authorize?") .refresh_token_url("https://www.example.com/token?") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - assert!(oauth.get(OAuthCredential::ClientId).is_some()); - oauth.remove(OAuthCredential::ClientId); - assert!(oauth.get(OAuthCredential::ClientId).is_none()); + assert!(oauth.get(OAuthParameter::ClientId).is_some()); + oauth.remove(OAuthParameter::ClientId); + assert!(oauth.get(OAuthParameter::ClientId).is_none()); oauth.client_id("client_id"); - assert!(oauth.get(OAuthCredential::ClientId).is_some()); + assert!(oauth.get(OAuthParameter::ClientId).is_some()); - assert!(oauth.get(OAuthCredential::RedirectUri).is_some()); - oauth.remove(OAuthCredential::RedirectUri); - assert!(oauth.get(OAuthCredential::RedirectUri).is_none()); + assert!(oauth.get(OAuthParameter::RedirectUri).is_some()); + oauth.remove(OAuthParameter::RedirectUri); + assert!(oauth.get(OAuthParameter::RedirectUri).is_none()); } #[test] fn setters() { // Doesn't matter the flow here as this is for testing // that the credentials are entered/retrieved correctly. - let mut oauth = OAuth::new(); + let mut oauth = OAuthSerializer::new(); oauth .client_id("client_id") .client_secret("client_secret") @@ -136,24 +136,21 @@ fn setters() { .redirect_uri("https://example.com/redirect") .authorization_code("access_code"); - let test_setter = |c: OAuthCredential, s: &str| { + let test_setter = |c: OAuthParameter, s: &str| { let result = oauth.get(c); assert!(result.is_some()); assert!(result.is_some()); assert_eq!(result.unwrap(), s); }; - test_setter(OAuthCredential::ClientId, "client_id"); - test_setter(OAuthCredential::ClientSecret, "client_secret"); + test_setter(OAuthParameter::ClientId, "client_id"); + test_setter(OAuthParameter::ClientSecret, "client_secret"); test_setter( - OAuthCredential::AuthorizationUrl, + OAuthParameter::AuthorizationUrl, "https://example.com/authorize", ); - test_setter( - OAuthCredential::RefreshTokenUrl, - "https://example.com/token", - ); - test_setter(OAuthCredential::AccessTokenUrl, "https://example.com/token"); - test_setter(OAuthCredential::RedirectUri, "https://example.com/redirect"); - test_setter(OAuthCredential::AuthorizationCode, "access_code"); + test_setter(OAuthParameter::RefreshTokenUrl, "https://example.com/token"); + test_setter(OAuthParameter::AccessTokenUrl, "https://example.com/token"); + test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); + test_setter(OAuthParameter::AuthorizationCode, "access_code"); } From b087b1357c07749184b17b7a7c5e5b15d3be75d0 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 13 May 2023 23:07:23 -0400 Subject: [PATCH 018/118] Add x509 pass method and compute - Move common methods to trait impl for credentials --- Cargo.toml | 1 - examples/oauth/auth_code_grant.rs | 12 +- examples/oauth/auth_code_grant_pkce.rs | 12 +- .../oauth/auth_code_grant_refresh_token.rs | 2 +- examples/oauth/implicit_grant.rs | 10 +- examples/oauth/main.rs | 4 +- examples/oauth_certificate/main.rs | 14 +- graph-oauth/Cargo.toml | 12 +- graph-oauth/src/auth_response_query.rs | 35 +- graph-oauth/src/identity/authority.rs | 12 +- .../auth_code_authorization_url.rs | 6 +- ...thorization_code_certificate_credential.rs | 109 ++---- .../authorization_code_credential.rs | 84 ++--- .../identity/credentials/client_assertion.rs | 202 ---------- .../client_certificate_credential.rs | 87 ++--- .../credentials/client_secret_credential.rs | 49 +-- .../confidential_client_application.rs | 4 +- .../credentials/credential_builder.rs | 60 +++ .../implicit_credential_authorization_url.rs | 126 +++---- graph-oauth/src/identity/credentials/mod.rs | 10 +- .../src/identity/credentials/test/cert.pem | 31 ++ .../src/identity/credentials/test/key.pem | 52 +++ .../identity/credentials/x509_certificate.rs | 346 ++++++++++++++++++ graph-oauth/src/lib.rs | 8 +- 24 files changed, 748 insertions(+), 540 deletions(-) delete mode 100644 graph-oauth/src/identity/credentials/client_assertion.rs create mode 100644 graph-oauth/src/identity/credentials/credential_builder.rs create mode 100644 graph-oauth/src/identity/credentials/test/cert.pem create mode 100644 graph-oauth/src/identity/credentials/test/key.pem create mode 100644 graph-oauth/src/identity/credentials/x509_certificate.rs diff --git a/Cargo.toml b/Cargo.toml index cdf87c79..d476290f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,4 +78,3 @@ debug = false name = "oauth_certificate_main" path = "examples/oauth_certificate/main.rs" required-features = ["openssl"] - diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 663d3d53..dbe94fe7 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::oauth::{ AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, - ConfidentialClientApplication, TokenRequest, + ConfidentialClientApplication, CredentialBuilder, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; @@ -25,16 +25,18 @@ pub fn authorization_sign_in() { webbrowser::open(url.as_str()).unwrap(); } -pub fn get_confidential_client(authorization_code: &str) -> ConfidentialClientApplication { +pub fn get_confidential_client( + authorization_code: &str, +) -> anyhow::Result<ConfidentialClientApplication> { let auth_code_credential = AuthorizationCodeCredential::builder() .with_authorization_code(authorization_code) .with_client_id(CLIENT_ID) .with_client_secret(CLIENT_SECRET) .with_scope(vec!["files.read", "offline_access"]) - .with_redirect_uri("http://localhost:8000/redirect") + .with_redirect_uri("http://localhost:8000/redirect")? .build(); - ConfidentialClientApplication::from(auth_code_credential) + Ok(ConfidentialClientApplication::from(auth_code_credential)) } /// # Example @@ -73,7 +75,7 @@ async fn handle_redirect( // Callers should handle the Result from requesting an access token // in case of an error here. let mut confidential_client_application = - get_confidential_client(access_code.code.as_str()); + get_confidential_client(access_code.code.as_str()).unwrap(); let response = confidential_client_application .get_token_async() diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 9e1e9bee..81ebf695 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, - ConfidentialClientApplication, ProofKeyForCodeExchange, TokenRequest, + ConfidentialClientApplication, CredentialBuilder, ProofKeyForCodeExchange, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; @@ -44,16 +44,18 @@ fn authorization_sign_in() { } /// Build the Authorization Code Grant Credential. -fn get_confidential_client_application(authorization_code: &str) -> ConfidentialClientApplication { +fn get_confidential_client_application( + authorization_code: &str, +) -> anyhow::Result<ConfidentialClientApplication> { let credential = AuthorizationCodeCredential::builder() .with_authorization_code(authorization_code) .with_client_id(CLIENT_ID) .with_client_secret(CLIENT_SECRET) - .with_redirect_uri("http://localhost:8000/redirect") + .with_redirect_uri("http://localhost:8000/redirect")? .with_proof_key_for_code_exchange(&PKCE) .build(); - ConfidentialClientApplication::from(credential) + Ok(ConfidentialClientApplication::from(credential)) } // When the authorization code comes in on the redirect from sign in, call the get_credential @@ -69,7 +71,7 @@ async fn handle_redirect( println!("{:#?}", access_code.code); let mut confidential_client = - get_confidential_client_application(access_code.code.as_str()); + get_confidential_client_application(access_code.code.as_str()).unwrap(); // Returns reqwest::Response let response = confidential_client.get_token_async().await.unwrap(); diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant_refresh_token.rs index 9d3ffd5f..e7855fd4 100644 --- a/examples/oauth/auth_code_grant_refresh_token.rs +++ b/examples/oauth/auth_code_grant_refresh_token.rs @@ -1,6 +1,6 @@ use graph_oauth::identity::AuthorizationCodeCredentialBuilder; use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, TokenRequest, + AuthorizationCodeCredential, ConfidentialClientApplication, CredentialBuilder, TokenRequest, }; // Use a refresh token to get a new access token. diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index 5b632b40..b6498f7d 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -16,10 +16,12 @@ // 2. Implicit grant flow for v2.0: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow // // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 -use graph_rs_sdk::oauth::{ImplicitCredentialAuthorizationUrl, Prompt, ResponseMode, ResponseType}; +use graph_rs_sdk::oauth::{ + CredentialBuilder, ImplicitCredential, Prompt, ResponseMode, ResponseType, +}; fn oauth_implicit_flow() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("<YOUR_CLIENT_ID>") .with_redirect_uri("http://localhost:8000/redirect") .with_prompt(Prompt::Login) @@ -42,13 +44,13 @@ fn oauth_implicit_flow() { } fn multi_response_types() { - let _ = ImplicitCredentialAuthorizationUrl::builder() + let _ = ImplicitCredential::builder() .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) .build(); // Or - let _ = ImplicitCredentialAuthorizationUrl::builder() + let _ = ImplicitCredential::builder() .with_response_type(ResponseType::FromString(vec![ "token".to_string(), "id_token".to_string(), diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index f4ce48ad..c27a53b6 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -30,7 +30,8 @@ mod signing_keys; use graph_rs_sdk::oauth::{ AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceAuthorizationCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenRequest, + CredentialBuilder, DeviceAuthorizationCredential, ProofKeyForCodeExchange, + PublicClientApplication, TokenRequest, }; #[tokio::main] @@ -63,6 +64,7 @@ async fn auth_code_grant(authorization_code: &str) { .with_client_id("CLIENT_ID") .with_client_secret("CLIENT_SECRET") .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() .with_proof_key_for_code_exchange(&pkce) .build(); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 1de5ce4d..f501459c 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,8 +4,8 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCertificateCredential, ClientAssertion, - ConfidentialClientApplication, PKey, TokenRequest, X509, + AccessToken, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, + CredentialBuilder, PKey, TokenRequest, X509Certificate, X509, }; use std::fs::File; use std::io::Read; @@ -74,6 +74,7 @@ pub fn get_confidential_client( client_id: &str, tenant_id: &str, ) -> anyhow::Result<ConfidentialClientApplication> { + // Use include_bytes!(file_path) if the files are local let mut cert_file = File::open(PRIVATE_KEY_PATH).unwrap(); let mut certificate: Vec<u8> = Vec::new(); cert_file.read_to_end(&mut certificate)?; @@ -85,16 +86,17 @@ pub fn get_confidential_client( let cert = X509::from_pem(certificate.as_slice()).unwrap(); let pkey = PKey::private_key_from_pem(private_key.as_slice()).unwrap(); - let signed_client_assertion = - ClientAssertion::new_with_tenant(client_id, tenant_id, cert, pkey); + let mut x509_certificate = X509Certificate::new(client_id, cert, pkey); + + x509_certificate.with_tenant(tenant_id); let credentials = AuthorizationCodeCertificateCredential::builder() .with_authorization_code(authorization_code) .with_client_id(client_id) .with_tenant(tenant_id) - .with_certificate(&signed_client_assertion)? + .with_certificate(&x509_certificate)? .with_scope(vec!["User.Read"]) - .with_redirect_uri("http://localhost:8080") + .with_redirect_uri("http://localhost:8080")? .build(); Ok(ConfidentialClientApplication::from(credentials)) diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 4fa021ca..fca0a1a1 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -11,8 +11,12 @@ description = "OAuth client implementing the OAuth 2.0 and OpenID Connect protoc keywords = ["microsoft", "oauth", "authentication", "authorization"] categories = ["authentication", "web-programming::http-client"] +exclude = [ + "src/identity/credentials/test/*" +] + [dependencies] -anyhow = "1.0.69" +anyhow = { version = "1.0.69", features = ["backtrace"]} async-trait = "0.1.35" base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } @@ -27,6 +31,7 @@ serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.24.1", features = ["derive"] } url = "2" +time = { version = "0.3.10", features = ["local-offset"] } webbrowser = "0.8.7" wry = "0.28.3" uuid = { version = "1.3.1", features = ["v4"] } @@ -43,3 +48,8 @@ brotli = ["reqwest/brotli"] deflate = ["reqwest/deflate"] trust-dns = ["reqwest/trust-dns"] openssl = ["dep:openssl"] + +[[test]] +name = "x509_certificate_tests" +path = "src/identity/credentials/x509_certificate.rs" +required-features = ["openssl"] diff --git a/graph-oauth/src/auth_response_query.rs b/graph-oauth/src/auth_response_query.rs index bb958a52..d524f8b1 100644 --- a/graph-oauth/src/auth_response_query.rs +++ b/graph-oauth/src/auth_response_query.rs @@ -1,16 +1,9 @@ -/* - let code = query.get("code"); - let id_token = query.get("id_token"); - let access_token = query.get("access_token"); - let state = query.get("state"); - let nonce = query.get("nonce"); -*/ - use serde_json::Value; use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; #[derive(Clone, Serialize, Deserialize)] -pub struct AuthResponseQuery { +pub struct AuthQueryResponse { pub code: Option<String>, pub id_token: Option<String>, pub access_token: Option<String>, @@ -21,3 +14,27 @@ pub struct AuthResponseQuery { #[serde(skip)] log_pii: bool, } + +impl Debug for AuthQueryResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.log_pii { + f.debug_struct("AuthQueryResponse") + .field("code", &self.code) + .field("id_token", &self.id_token) + .field("access_token", &self.access_token) + .field("state", &self.state) + .field("nonce", &self.nonce) + .field("additional_fields(serde flatten)", &self.additional_fields) + .finish() + } else { + f.debug_struct("AuthQueryResponse") + .field("code", &self.code) + .field("id_token", &"[REDACTED]") + .field("access_token", &"[REDACTED]") + .field("state", &self.state) + .field("nonce", &self.nonce) + .field("additional_fields(serde flatten)", &self.additional_fields) + .finish() + } + } +} diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 79d96875..560b1bc4 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -23,7 +23,6 @@ pub enum AzureAuthorityHost { impl AsRef<str> for AzureAuthorityHost { fn as_ref(&self) -> &str { match self { - //AzureAuthorityHost::Custom(url) => url.as_str(), AzureAuthorityHost::AzurePublic => "https://login.microsoftonline.com", AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn", AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de", @@ -50,7 +49,6 @@ impl AzureAuthorityHost { pub fn default_managed_identity_scope(&self) -> &'static str { match self { - //AzureAuthorityHost::Custom(_) => "https://management.azure.com//.default", AzureAuthorityHost::AzurePublic => "https://management.azure.com//.default", AzureAuthorityHost::AzureChina => "https://management.chinacloudapi.cn/.default", AzureAuthorityHost::AzureGermany => "https://management.microsoftazure.de/.default", @@ -74,6 +72,15 @@ pub enum Authority { TenantId(String), } +impl Authority { + pub fn tenant_id(&self) -> Option<&String> { + match self { + Authority::TenantId(tenant_id) => Some(tenant_id), + _ => None, + } + } +} + impl AsRef<str> for Authority { fn as_ref(&self) -> &str { match self { @@ -95,6 +102,7 @@ impl ToString for Authority { impl From<&str> for Authority { fn from(value: &str) -> Self { match value.as_bytes() { + b"aad" => Authority::AzureActiveDirectory, b"common" => Authority::Common, b"adfs" => Authority::AzureDirectoryFederatedServices, b"organizations" => Authority::Organizations, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 58408eec..d58d930a 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::auth_response_query::AuthResponseQuery; +use crate::auth_response_query::AuthQueryResponse; use crate::identity::{ Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, }; @@ -95,7 +95,7 @@ impl AuthCodeAuthorizationUrl { pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, - ) -> anyhow::Result<AuthResponseQuery> { + ) -> anyhow::Result<AuthQueryResponse> { let url_string = self .interactive_authentication(interactive_web_view_options)? .ok_or(anyhow::Error::msg( @@ -133,7 +133,7 @@ impl AuthCodeAuthorizationUrl { )), ))?; - let response_query: AuthResponseQuery = serde_urlencoded::from_str(query)?; + let response_query: AuthQueryResponse = serde_urlencoded::from_str(query)?; Ok(response_query) } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index f1fbf4c8..261fe536 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -2,15 +2,22 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, - AzureAuthorityHost, TokenCredentialOptions, TokenRequest, + AzureAuthorityHost, CredentialBuilder, TokenCredentialOptions, TokenRequest, + CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; #[cfg(feature = "openssl")] -use crate::oauth::ClientAssertion; +use crate::oauth::X509Certificate; + +credential_builder_impl!( + AuthorizationCodeCertificateCredentialBuilder, + AuthorizationCodeCertificateCredential +); /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application /// to obtain authorized access to protected resources like web APIs. The auth code flow requires @@ -18,7 +25,7 @@ use crate::oauth::ClientAssertion; /// identity platform) back to your application. For example, a web browser, desktop, or mobile /// application operated by a user to sign in to your app and access their data. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct AuthorizationCodeCertificateCredential { /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. pub(crate) authorization_code: Option<String>, @@ -33,7 +40,7 @@ pub struct AuthorizationCodeCertificateCredential { /// The redirect_uri of your app, where authentication responses can be sent and received /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. - pub(crate) redirect_uri: String, + pub(crate) redirect_uri: Url, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. @@ -58,27 +65,27 @@ pub struct AuthorizationCodeCertificateCredential { } impl AuthorizationCodeCertificateCredential { - pub fn new<T: AsRef<str>>( + pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, - _client_secret: T, authorization_code: T, - redirect_uri: T, client_assertion: T, - ) -> AuthorizationCodeCertificateCredential { - AuthorizationCodeCertificateCredential { + redirect_uri: U, + ) -> AuthorizationResult<AuthorizationCodeCertificateCredential> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + + Ok(AuthorizationCodeCertificateCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), + redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, code_verifier: None, - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" - .to_owned(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), scope: vec![], authority: Default::default(), token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), - } + }) } pub fn builder() -> AuthorizationCodeCertificateCredentialBuilder { @@ -109,17 +116,6 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg_result( - &format!( - "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - Some("Authorization code and refresh token cannot be set at the same time - choose one or the other"), - ); - } - if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( OAuthParameter::ClientId.alias(), @@ -154,7 +150,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if refresh_token.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( OAuthParameter::RefreshToken.alias(), - None, + Some("refresh_token is an empty string"), ); } @@ -166,15 +162,15 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { SerializerField::Required(OAuthParameter::RefreshToken), SerializerField::Required(OAuthParameter::ClientId), SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), SerializerField::Required(OAuthParameter::ClientAssertion), SerializerField::Required(OAuthParameter::ClientAssertionType), + SerializerField::NotRequired(OAuthParameter::Scope), ]); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AuthorizationFailure::required_value_msg_result( OAuthParameter::AuthorizationCode.alias(), - Some("refresh_token is set but is empty"), + Some("authorization_code is an empty string"), ); } @@ -187,10 +183,10 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { SerializerField::Required(OAuthParameter::ClientId), SerializerField::Required(OAuthParameter::RedirectUri), SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), - SerializerField::NotRequired(OAuthParameter::CodeVerifier), SerializerField::Required(OAuthParameter::ClientAssertion), SerializerField::Required(OAuthParameter::ClientAssertionType), + SerializerField::NotRequired(OAuthParameter::Scope), + SerializerField::NotRequired(OAuthParameter::CodeVerifier), ]); } @@ -216,11 +212,12 @@ impl AuthorizationCodeCertificateCredentialBuilder { credential: AuthorizationCodeCertificateCredential { authorization_code: None, refresh_token: None, - client_id: String::new(), - redirect_uri: String::new(), + client_id: String::with_capacity(32), + redirect_uri: Url::parse("http://localhost") + .expect("Internal Error - please report"), code_verifier: None, client_assertion_type: String::new(), - client_assertion: String::new(), + client_assertion: CLIENT_ASSERTION_TYPE.to_owned(), scope: vec![], authority: Default::default(), token_credential_options: TokenCredentialOptions::default(), @@ -240,24 +237,9 @@ impl AuthorizationCodeCertificateCredentialBuilder { self } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); - self - } - - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self - } - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); - self + pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { + self.credential.redirect_uri = redirect_uri.into_url()?; + Ok(self) } pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { @@ -268,10 +250,13 @@ impl AuthorizationCodeCertificateCredentialBuilder { #[cfg(feature = "openssl")] pub fn with_certificate( &mut self, - certificate_assertion: &ClientAssertion, + certificate_assertion: &X509Certificate, ) -> anyhow::Result<&mut Self> { - self.with_client_assertion(certificate_assertion.sign()?); - self.with_client_assertion_type("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); + if let Some(tenant_id) = self.credential.authority.tenant_id() { + self.with_client_assertion(certificate_assertion.sign(Some(tenant_id.clone()))?); + } else { + self.with_client_assertion(certificate_assertion.sign(None)?); + } Ok(self) } @@ -287,31 +272,15 @@ impl AuthorizationCodeCertificateCredentialBuilder { self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); self } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scope = scopes.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } - - pub fn build(&self) -> AuthorizationCodeCertificateCredential { - self.credential.clone() - } } impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBuilder { fn from(value: AuthCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); + let _ = builder.with_redirect_uri(value.redirect_uri); builder .with_scope(value.scope) .with_client_id(value.client_id) - .with_redirect_uri(value.redirect_uri) .with_authority(value.authority); builder diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 708ac4a3..ae8ccf8b 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -2,14 +2,20 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, - ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, + CredentialBuilder, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, }; use crate::oauth::AuthCodeAuthorizationUrlBuilder; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; +credential_builder_impl!( + AuthorizationCodeCredentialBuilder, + AuthorizationCodeCredential +); + /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application /// to obtain authorized access to protected resources like web APIs. The auth code flow requires /// a user-agent that supports redirection from the authorization server (the Microsoft @@ -41,7 +47,7 @@ pub struct AuthorizationCodeCredential { /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, /// The same redirect_uri value that was used to acquire the authorization_code. - pub(crate) redirect_uri: String, + pub(crate) redirect_uri: Url, /// A space-separated list of scopes. The scopes must all be from a single resource, /// along with OIDC scopes (profile, openid, email). For more information, see Permissions /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension @@ -59,24 +65,26 @@ pub struct AuthorizationCodeCredential { } impl AuthorizationCodeCredential { - pub fn new<T: AsRef<str>>( + pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, client_secret: T, authorization_code: T, - redirect_uri: T, - ) -> AuthorizationCodeCredential { - AuthorizationCodeCredential { + redirect_uri: U, + ) -> AuthorizationResult<AuthorizationCodeCredential> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + + Ok(AuthorizationCodeCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), + redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, scope: vec![], authority: Default::default(), code_verifier: None, token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), - } + }) } pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { @@ -125,17 +133,6 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg_result( - &format!( - "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - Some("Authorization code and refresh token should not be set at the same time - Internal Error"), - ); - } - if self.client_id.trim().is_empty() { return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); } @@ -178,10 +175,6 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { ); } - if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::RedirectUri); - } - self.serializer .authorization_code(authorization_code.as_ref()) .grant_type("authorization_code") @@ -230,7 +223,8 @@ impl AuthorizationCodeCredentialBuilder { refresh_token: None, client_id: String::new(), client_secret: String::new(), - redirect_uri: String::new(), + redirect_uri: Url::parse("http://localhost") + .expect("Internal Error - please report"), scope: vec![], authority: Default::default(), code_verifier: None, @@ -252,14 +246,10 @@ impl AuthorizationCodeCredentialBuilder { self } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); - self - } - - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self + /// Defaults to http://localhost + pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { + self.credential.redirect_uri = redirect_uri.into_url()?; + Ok(self) } pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { @@ -267,17 +257,6 @@ impl AuthorizationCodeCredentialBuilder { self } - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); - self - } - pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); self @@ -290,31 +269,15 @@ impl AuthorizationCodeCredentialBuilder { self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); self } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scope = scopes.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } - - pub fn build(&self) -> AuthorizationCodeCredential { - self.credential.clone() - } } impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { fn from(value: AuthCodeAuthorizationUrl) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); + let _ = builder.with_redirect_uri(value.redirect_uri); builder .with_scope(value.scope) .with_client_id(value.client_id) - .with_redirect_uri(value.redirect_uri) .with_authority(value.authority); builder @@ -355,6 +318,7 @@ mod test { let mut credential_builder = AuthorizationCodeCredential::builder(); credential_builder .with_redirect_uri("https://localhost:8080") + .unwrap() .with_client_id("client_id") .with_client_secret("client_secret") .with_scope(vec!["scope"]) diff --git a/graph-oauth/src/identity/credentials/client_assertion.rs b/graph-oauth/src/identity/credentials/client_assertion.rs deleted file mode 100644 index 15308356..00000000 --- a/graph-oauth/src/identity/credentials/client_assertion.rs +++ /dev/null @@ -1,202 +0,0 @@ -use anyhow::Context; -use base64::Engine; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::rsa::Padding; -use openssl::sign::Signer; -use openssl::x509::X509; -use std::collections::HashMap; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use uuid::Uuid; - -/// Computes the client assertion used in certificate credential authorization flows. -/// The client assertion is computed from the DER encoding of an X509 certificate and it's private key. -/// -/// Client assertions are generated using the openssl library for security reasons. -/// You can see an example of how this is done by Microsoft located at -/// https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions -pub struct ClientAssertion { - client_id: String, - tenant_id: Option<String>, - claims: Option<HashMap<String, String>>, - extend_claims: bool, - certificate: X509, - pkey: PKey<Private>, - uuid: Uuid, -} - -impl ClientAssertion { - pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { - Self { - client_id: client_id.as_ref().to_owned(), - tenant_id: None, - claims: None, - extend_claims: true, - certificate, - pkey: private_key, - uuid: Uuid::new_v4(), - } - } - - pub fn new_with_tenant<T: AsRef<str>>( - client_id: T, - tenant_id: T, - certificate: X509, - private_key: PKey<Private>, - ) -> ClientAssertion { - Self { - client_id: client_id.as_ref().to_owned(), - tenant_id: Some(tenant_id.as_ref().to_owned()), - claims: None, - extend_claims: true, - certificate, - pkey: private_key, - uuid: Uuid::new_v4(), - } - } - - /// Provide your own set of claims in the payload of the JWT. - /// - /// Set extend_claims to false in order to replace the claims that would be generated - /// for the client assertion. This replaces the following payload fields: aud, exp, nbf, jti, - /// sub, and iss. This ensures that only the claims given are passed for the payload of the JWT - /// used in the client assertion. - /// - /// If extend claims is true, the claims provided are in addition - /// to those claims mentioned above and do not replace them, however, any claim provided - /// with the same fields above will replace those that are generated. - pub fn with_claims(&mut self, claims: HashMap<String, String>, extend_claims: bool) { - self.claims = Some(claims); - self.extend_claims = extend_claims; - } - - /// Hex encoded SHA-1 thumbprint of the X.509 certificate's DER encoding. - /// - /// You can verify that the correct certificate has been passed - /// by comparing the hex encoded thumbprint against the thumbprint given in Azure - /// Active Directory under Certificates and Secrets for your application or by looking - /// at the keyCredentials customKeyIdentifier field in your applications manifest. - pub fn get_thumbprint(&self) -> Result<String, ErrorStack> { - let digest_bytes = self.certificate.digest(MessageDigest::sha1())?; - Ok(hex::encode(digest_bytes.as_ref()).to_uppercase()) - } - - /// Base64 Url encoded (No Pad) SHA-1 thumbprint of the X.509 certificate's DER encoding. - pub fn get_thumbprint_base64(&self) -> Result<String, ErrorStack> { - let digest_bytes = self.certificate.digest(MessageDigest::sha1())?; - Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest_bytes)) - } - - /// Get the value used for the jti field in the payload. This field is computed - /// when constructing the [ClientAssertion] and will be different from any - /// custom claims provided. - /// - /// The "jti" (JWT ID) claim provides a unique identifier for the JWT. - /// The identifier value MUST be assigned in a manner that ensures that there is - /// a negligible probability that the same value will be accidentally assigned to - /// a different data object; if the application uses multiple issuers, collisions - /// MUST be prevented among values produced by different issuers as well. - pub fn get_uuid(&self) -> &Uuid { - &self.uuid - } - - /// Set the UUID for the jti field of the claims/payload of the jwt. - pub fn set_uuid(&mut self, value: Uuid) { - self.uuid = value; - } - - fn get_header(&self) -> Result<HashMap<String, String>, ErrorStack> { - let mut header = HashMap::new(); - header.insert("x5t".to_owned(), self.get_thumbprint_base64()?); - header.insert("alg".to_owned(), "RS256".to_owned()); - header.insert("typ".to_owned(), "JWT".to_owned()); - Ok(header) - } - - fn get_claims(&self) -> anyhow::Result<HashMap<String, String>> { - if let Some(claims) = self.claims.as_ref() { - if !self.extend_claims { - return Ok(claims.clone()); - } - } - - let aud = { - if let Some(tenant_id) = self.tenant_id.as_ref() { - format!( - "https://login.microsoftonline.com/{}/oauth2/v2.0/token", - tenant_id - ) - } else { - "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_owned() - } - }; - - // 10 minutes until expiration. - let exp = 60 * 10; - let nbf = SystemTime::now().duration_since(UNIX_EPOCH)?; - let exp = nbf - .checked_add(Duration::from_secs(exp)) - .context("Unable to set exp claims field - Reason: Unknown")?; - - let mut claims = HashMap::new(); - claims.insert("aud".to_owned(), aud); - claims.insert("exp".to_owned(), exp.as_secs().to_string()); - claims.insert("nbf".to_owned(), nbf.as_secs().to_string()); - claims.insert("jti".to_owned(), self.uuid.to_string()); - claims.insert("sub".to_owned(), self.client_id.to_owned()); - claims.insert("iss".to_owned(), self.client_id.to_owned()); - - if let Some(internal_claims) = self.claims.as_ref() { - claims.extend(internal_claims.clone()); - } - - Ok(claims) - } - - /// JWT Header and Payload in the format header.payload - fn base64_token(&self) -> anyhow::Result<String> { - let header = self.get_header()?; - let header = serde_json::to_string(&header)?; - let header_base64 = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes()); - let claims = self.get_claims()?; - let claims = serde_json::to_string(&claims)?; - let claims_base64 = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes()); - Ok(format!("{}.{}", header_base64, claims_base64)) - } - - /* - Altogether the general flow is as follows: - - let header = self.get_header()?; - let header = serde_json::to_string(&header).unwrap(); - let header_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes()); - let claims = self.get_claims(); - let claims = serde_json::to_string(&claims).unwrap(); - let claims_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes()); - let token = format!("{}.{}", header_base64, claims_base64); - - let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; - signer.set_rsa_padding(Padding::PKCS1)?; - signer.update(token.as_str().as_bytes())?; - let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); - let signed_client_assertion = format!("{token}.{signature}"); - Ok(signed_client_assertion) - */ - - /// Get the signed client assertion. - /// - /// The signature is a Base64 Url encoded (No Pad) JWT Header and Payload signed with the private key using SHA_256 - /// and RSA padding PKCS1 - pub fn sign(&self) -> anyhow::Result<String> { - let token = self.base64_token()?; - let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; - signer.set_rsa_padding(Padding::PKCS1)?; - signer.update(token.as_str().as_bytes())?; - let signature = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); - Ok(format!("{token}.{signature}")) - } -} diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index c8825dd5..5d7852d9 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,7 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::form_credential::SerializerField; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, + Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, + TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; @@ -9,10 +10,16 @@ use std::collections::HashMap; use url::Url; #[cfg(feature = "openssl")] -use crate::identity::ClientAssertion; +use crate::identity::X509Certificate; use crate::oauth::ClientCredentialsAuthorizationUrlBuilder; -static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; +pub(crate) static CLIENT_ASSERTION_TYPE: &str = + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; + +credential_builder_impl!( + ClientCertificateCredentialBuilder, + ClientCertificateCredential +); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #[derive(Clone)] @@ -24,7 +31,7 @@ pub struct ClientCertificateCredential { /// identifier (application ID URI) of the resource you want, affixed with the .default /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, pub(crate) authority: Authority, pub(crate) client_assertion_type: String, pub(crate) client_assertion: String, @@ -37,7 +44,7 @@ impl ClientCertificateCredential { pub fn new<T: AsRef<str>>(client_id: T, client_assertion: T) -> ClientCertificateCredential { ClientCertificateCredential { client_id: client_id.as_ref().to_owned(), - scopes: vec![], + scope: vec!["https://graph.microsoft.com/.default".into()], authority: Default::default(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), @@ -112,7 +119,7 @@ impl AuthorizationSerializer for ClientCertificateCredential { .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()); - if self.scopes.is_empty() { + if self.scope.is_empty() { self.serializer .add_scope("https://graph.microsoft.com/.default"); } @@ -133,18 +140,18 @@ impl AuthorizationSerializer for ClientCertificateCredential { SerializerField::Required(OAuthParameter::RefreshToken), SerializerField::Required(OAuthParameter::ClientId), SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), SerializerField::Required(OAuthParameter::ClientAssertion), SerializerField::Required(OAuthParameter::ClientAssertionType), + SerializerField::NotRequired(OAuthParameter::Scope), ]) } else { self.serializer.grant_type("client_credentials"); self.serializer.authorization_form(vec![ SerializerField::Required(OAuthParameter::ClientId), SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), SerializerField::Required(OAuthParameter::ClientAssertion), SerializerField::Required(OAuthParameter::ClientAssertionType), + SerializerField::NotRequired(OAuthParameter::Scope), ]) }; } @@ -158,8 +165,8 @@ impl ClientCertificateCredentialBuilder { fn new() -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { - client_id: String::new(), - scopes: vec![], + client_id: String::with_capacity(32), + scope: vec!["https://graph.microsoft.com/.default".into()], authority: Default::default(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), @@ -170,17 +177,16 @@ impl ClientCertificateCredentialBuilder { } } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self - } - #[cfg(feature = "openssl")] pub fn with_certificate( &mut self, - certificate_assertion: &ClientAssertion, + certificate_assertion: &X509Certificate, ) -> anyhow::Result<&mut Self> { - self.with_client_assertion(certificate_assertion.sign()?); + if let Some(tenant_id) = self.credential.authority.tenant_id() { + self.with_client_assertion(certificate_assertion.sign(Some(tenant_id.clone()))?); + } else { + self.with_client_assertion(certificate_assertion.sign(None)?); + } Ok(self) } @@ -193,38 +199,33 @@ impl ClientCertificateCredentialBuilder { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } +} - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); - self +impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder { + fn from(credential: ClientCertificateCredential) -> Self { + ClientCertificateCredentialBuilder { credential } } +} - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); - self - } +#[cfg(test)] +mod test { + use super::*; - /// Defaults to "https://graph.microsoft.com/.default" - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.credential.scopes = scopes.into_iter().map(|s| s.to_string()).collect(); - self - } + static TEST_CLIENT_ID: &str = "671a21bd-b91b-8ri7-94cb-e2cea49f30e1"; - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } + #[test] + fn credential_builder() { + let mut builder = ClientCertificateCredentialBuilder::new(); + builder.with_client_id(TEST_CLIENT_ID); + assert_eq!(builder.credential.client_id, TEST_CLIENT_ID); - pub fn build(&self) -> ClientCertificateCredential { - self.credential.clone() - } -} + builder.with_client_id("123"); + assert_eq!(builder.credential.client_id, "123"); -impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder { - fn from(credential: ClientCertificateCredential) -> Self { - ClientCertificateCredentialBuilder { credential } + builder.credential.client_id = "".into(); + assert!(builder.credential.client_id.is_empty()); + + builder.with_client_id(TEST_CLIENT_ID); + assert_eq!(builder.credential.client_id, TEST_CLIENT_ID); } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 3692cb82..aca831d4 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -2,13 +2,15 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, - ClientCredentialsAuthorizationUrlBuilder, TokenRequest, + ClientCredentialsAuthorizationUrlBuilder, CredentialBuilder, TokenRequest, }; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; +credential_builder_impl!(ClientSecretCredentialBuilder, ClientSecretCredential); + /// Client Credentials flow using a client secret. /// /// The OAuth 2.0 client credentials grant flow permits a web service (confidential client) @@ -39,7 +41,7 @@ pub struct ClientSecretCredential { /// identifier (application ID URI) of the resource you want, affixed with the .default /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. - pub(crate) scopes: Vec<String>, + pub(crate) scope: Vec<String>, pub(crate) authority: Authority, pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, @@ -50,7 +52,7 @@ impl ClientSecretCredential { ClientSecretCredential { client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - scopes: vec![], + scope: vec!["https://graph.microsoft.com/.default".into()], authority: Default::default(), token_credential_options: Default::default(), serializer: OAuthSerializer::new(), @@ -65,7 +67,7 @@ impl ClientSecretCredential { ClientSecretCredential { client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - scopes: vec![], + scope: vec!["https://graph.microsoft.com/.default".into()], authority: Authority::TenantId(tenant_id.as_ref().to_owned()), token_credential_options: Default::default(), serializer: OAuthSerializer::new(), @@ -109,11 +111,11 @@ impl AuthorizationSerializer for ClientSecretCredential { self.serializer.grant_type("client_credentials"); - if self.scopes.is_empty() { + if self.scope.is_empty() { self.serializer .extend_scopes(vec!["https://graph.microsoft.com/.default".to_owned()]); } else { - self.serializer.extend_scopes(&self.scopes); + self.serializer.extend_scopes(&self.scope); } self.serializer.authorization_form(vec![ @@ -138,7 +140,7 @@ impl ClientSecretCredentialBuilder { credential: ClientSecretCredential { client_id: String::new(), client_secret: String::new(), - scopes: vec![], + scope: vec![], authority: Default::default(), token_credential_options: Default::default(), serializer: Default::default(), @@ -146,43 +148,10 @@ impl ClientSecretCredentialBuilder { } } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self - } - pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { self.credential.client_secret = client_secret.as_ref().to_owned(); self } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); - self - } - - /// Defaults to "https://graph.microsoft.com/.default" - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.credential.scopes = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } - - pub fn build(&self) -> ClientSecretCredential { - self.credential.clone() - } } impl Default for ClientSecretCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index bd583796..e284c60a 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -171,7 +171,7 @@ impl From<ClientCertificateCredential> for ConfidentialClientApplication { #[cfg(test)] mod test { use super::*; - use crate::identity::{Authority, AzureAuthorityHost}; + use crate::identity::{Authority, AzureAuthorityHost, CredentialBuilder}; #[test] fn confidential_client_new() { @@ -181,6 +181,7 @@ mod test { .with_client_secret("CLDIE3F") .with_scope(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() .build(); let mut confidential_client = @@ -205,6 +206,7 @@ mod test { .with_client_secret("CLDIE3F") .with_scope(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() .with_authority(Authority::Consumers) .build(); let mut confidential_client = diff --git a/graph-oauth/src/identity/credentials/credential_builder.rs b/graph-oauth/src/identity/credentials/credential_builder.rs new file mode 100644 index 00000000..7a2a9e4e --- /dev/null +++ b/graph-oauth/src/identity/credentials/credential_builder.rs @@ -0,0 +1,60 @@ +use crate::identity::{Authority, TokenCredentialOptions}; + +pub trait CredentialBuilder { + type Credential; + + fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self; + fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self; + fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self; + fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self; + fn with_token_credential_options(&mut self, options: TokenCredentialOptions) -> &mut Self; + fn build(&self) -> Self::Credential; +} + +macro_rules! credential_builder_impl { + ($name:ident, $credential:ty) => { + impl CredentialBuilder for $name { + type Credential = $credential; + + fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + if self.credential.client_id.is_empty() { + self.credential.client_id.push_str(client_id.as_ref()); + } else { + self.credential.client_id = client_id.as_ref().to_owned(); + } + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { + self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + fn with_scope<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scope: I, + ) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + fn with_token_credential_options( + &mut self, + options: TokenCredentialOptions, + ) -> &mut Self { + self.credential.token_credential_options = options; + self + } + + fn build(&self) -> $credential { + self.credential.clone() + } + } + }; +} diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index 32741218..be01d16e 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -1,20 +1,22 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::form_credential::SerializerField; -use crate::identity::{Authority, AzureAuthorityHost, ResponseMode}; -use crate::oauth::{Prompt, ResponseType}; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; +use crate::identity::{ + Authority, AzureAuthorityHost, CredentialBuilder, Crypto, Prompt, ResponseMode, ResponseType, +}; +use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; -use ring::rand::SecureRandom; use url::form_urlencoded::Serializer; use url::Url; + +credential_builder_impl!(ImplicitCredentialBuilder, ImplicitCredential); + /// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens) /// are returned directly from the /authorize endpoint instead of the /token endpoint. This is /// often used as part of the authorization code flow, in what is called the "hybrid flow" - /// retrieving the ID token on the /authorize request along with an authorization code. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow #[derive(Clone)] -pub struct ImplicitCredentialAuthorizationUrl { +pub struct ImplicitCredential { /// Required. /// The Application (client) ID that the Azure portal - App registrations page assigned /// to your app @@ -33,6 +35,8 @@ pub struct ImplicitCredentialAuthorizationUrl { /// The redirect_uri of your app, where authentication responses can be sent and received /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. + /// + /// URL-encoding is done for you in the sdk client. pub(crate) redirect_uri: Option<String>, /// Required /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the @@ -85,15 +89,18 @@ pub struct ImplicitCredentialAuthorizationUrl { pub(crate) domain_hint: Option<String>, /// The Azure Active Directory tenant (directory) Id of the service principal. pub(crate) authority: Authority, + /// [ImplicitCredential] does not use TokenCredentialOptions. + /// This is here for compatibility with the [CredentialBuilder] trait. + token_credential_options: TokenCredentialOptions, } -impl ImplicitCredentialAuthorizationUrl { +impl ImplicitCredential { pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( client_id: T, nonce: T, scope: I, - ) -> ImplicitCredentialAuthorizationUrl { - ImplicitCredentialAuthorizationUrl { + ) -> ImplicitCredential { + ImplicitCredential { client_id: client_id.as_ref().to_owned(), response_type: vec![ResponseType::Token], redirect_uri: None, @@ -105,11 +112,12 @@ impl ImplicitCredentialAuthorizationUrl { login_hint: None, domain_hint: None, authority: Default::default(), + token_credential_options: Default::default(), } } - pub fn builder() -> ImplicitCredentialAuthorizationUrlBuilder { - ImplicitCredentialAuthorizationUrlBuilder::new() + pub fn builder() -> ImplicitCredentialBuilder { + ImplicitCredentialBuilder::new() } pub fn url(&self) -> AuthorizationResult<Url> { @@ -229,21 +237,21 @@ impl ImplicitCredentialAuthorizationUrl { } #[derive(Clone)] -pub struct ImplicitCredentialAuthorizationUrlBuilder { - implicit_credential_authorization_url: ImplicitCredentialAuthorizationUrl, +pub struct ImplicitCredentialBuilder { + credential: ImplicitCredential, } -impl Default for ImplicitCredentialAuthorizationUrlBuilder { +impl Default for ImplicitCredentialBuilder { fn default() -> Self { Self::new() } } -impl ImplicitCredentialAuthorizationUrlBuilder { - pub fn new() -> ImplicitCredentialAuthorizationUrlBuilder { - ImplicitCredentialAuthorizationUrlBuilder { - implicit_credential_authorization_url: ImplicitCredentialAuthorizationUrl { - client_id: String::new(), +impl ImplicitCredentialBuilder { + pub fn new() -> ImplicitCredentialBuilder { + ImplicitCredentialBuilder { + credential: ImplicitCredential { + client_id: String::with_capacity(32), response_type: vec![ResponseType::Code], redirect_uri: None, scope: vec![], @@ -254,30 +262,13 @@ impl ImplicitCredentialAuthorizationUrlBuilder { login_hint: None, domain_hint: None, authority: Default::default(), + token_credential_options: Default::default(), }, } } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.implicit_credential_authorization_url.client_id = client_id.as_ref().to_owned(); - self - } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.implicit_credential_authorization_url.redirect_uri = - Some(redirect_uri.as_ref().to_owned()); - self - } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.implicit_credential_authorization_url.authority = - Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.implicit_credential_authorization_url.authority = authority.into(); + self.credential.redirect_uri = Some(redirect_uri.as_ref().to_owned()); self } @@ -287,8 +278,7 @@ impl ImplicitCredentialAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.implicit_credential_authorization_url.response_type = - response_type.into_iter().collect(); + self.credential.response_type = response_type.into_iter().collect(); self } @@ -304,7 +294,7 @@ impl ImplicitCredentialAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.implicit_credential_authorization_url.response_mode = response_mode; + self.credential.response_mode = response_mode; self } @@ -313,7 +303,7 @@ impl ImplicitCredentialAuthorizationUrlBuilder { /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.implicit_credential_authorization_url.nonce = nonce.as_ref().to_owned(); + self.credential.nonce = nonce.as_ref().to_owned(); self } @@ -329,28 +319,12 @@ impl ImplicitCredentialAuthorizationUrlBuilder { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - let mut buf = [0; 32]; - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; - let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); - - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(base_64_random_string.as_bytes()); - - let nonce = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - self.implicit_credential_authorization_url.nonce = nonce; + self.credential.nonce = Crypto::secure_random_string()?; Ok(self) } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.implicit_credential_authorization_url.state = Some(state.as_ref().to_owned()); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.implicit_credential_authorization_url.scope = - scope.into_iter().map(|s| s.to_string()).collect(); + self.credential.state = Some(state.as_ref().to_owned()); self } @@ -365,28 +339,22 @@ impl ImplicitCredentialAuthorizationUrlBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.implicit_credential_authorization_url.prompt = Some(prompt); + self.credential.prompt = Some(prompt); self } pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.implicit_credential_authorization_url.domain_hint = - Some(domain_hint.as_ref().to_owned()); + self.credential.domain_hint = Some(domain_hint.as_ref().to_owned()); self } pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.implicit_credential_authorization_url.login_hint = - Some(login_hint.as_ref().to_owned()); + self.credential.login_hint = Some(login_hint.as_ref().to_owned()); self } - pub fn build(&self) -> ImplicitCredentialAuthorizationUrl { - self.implicit_credential_authorization_url.clone() - } - pub fn url(&self) -> AuthorizationResult<Url> { - self.implicit_credential_authorization_url.url() + self.credential.url() } } @@ -396,7 +364,7 @@ mod test { #[test] fn serialize_uri() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") @@ -414,7 +382,7 @@ mod test { #[test] fn set_open_id_fragment() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) @@ -432,7 +400,7 @@ mod test { #[test] fn set_open_id_fragment2() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https::/localhost:8080/myapp") @@ -449,7 +417,7 @@ mod test { #[test] fn response_type_join() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") @@ -466,7 +434,7 @@ mod test { #[test] fn response_type_join_string() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::FromString(vec![ "id_token".to_owned(), @@ -486,7 +454,7 @@ mod test { #[test] fn response_type_into_iter() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::IdToken) .with_redirect_uri("https::/localhost:8080/myapp") @@ -503,7 +471,7 @@ mod test { #[test] fn response_type_into_iter2() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") @@ -521,7 +489,7 @@ mod test { #[test] #[should_panic] fn missing_scope_panic() { - let authorizer = ImplicitCredentialAuthorizationUrl::builder() + let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") @@ -533,7 +501,7 @@ mod test { #[test] fn generate_nonce() { - let url = ImplicitCredentialAuthorizationUrl::builder() + let url = ImplicitCredential::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index a4d8041a..c837d448 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,3 +1,6 @@ +#[macro_use] +mod credential_builder; + mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; @@ -26,7 +29,7 @@ mod token_flow_authorization_url; mod token_request; #[cfg(feature = "openssl")] -mod client_assertion; +mod x509_certificate; pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; @@ -37,7 +40,8 @@ pub use client_secret_credential::*; pub use code_flow_authorization_url::*; pub use code_flow_credential::*; pub use confidential_client_application::*; -pub use crypto::*; +pub use credential_builder::*; +pub(crate) use crypto::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; @@ -56,4 +60,4 @@ pub use token_flow_authorization_url::*; pub use token_request::*; #[cfg(feature = "openssl")] -pub use client_assertion::*; +pub use x509_certificate::*; diff --git a/graph-oauth/src/identity/credentials/test/cert.pem b/graph-oauth/src/identity/credentials/test/cert.pem new file mode 100644 index 00000000..d76d70d8 --- /dev/null +++ b/graph-oauth/src/identity/credentials/test/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFWzCCA0OgAwIBAgIUCdZMehPGhXiar01p4lwWc/dF4EgwDQYJKoZIhvcNAQEL +BQAwPTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAlNDMSEwHwYDVQQKDBhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMwNDIxMDUxMjU0WhcNMjQwNDIwMDUxMjU0 +WjA9MQswCQYDVQQGEwJVUzELMAkGA1UECAwCU0MxITAfBgNVBAoMGEludGVybmV0 +IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALBxyQJzREWx7f1DC43JceL7lCq9jV0qcihBIh4s4ZXCfCH4lsdDXNuReiqIkioJ +XyQIy/tAsPyY88DP6q6Q4xunm0vh0DLjxd/bAO8gVLuyz1Uzk4uOWtXiyGZwcm2M +eig5SKRHcu5NRPW+++eeESqqyNHGrn1QphK+jBNht8+PmGC0uXjUu1ggTTblwJDm +wBobN0Kq40CRrI4eZMoHFDS3p40h/0Kfux+PvtdDRqUpUKhaxf7lfD9kclADFCev +IadmBbNRtj1X9lWTyxNgnTbYS7OTJFxiBTqzWdxk1KIutShA9mD0sdqa9rVqAMSP +GygD1LE/rmdJXrRqXtbePPbhhD25rd7Ny3Rc+LgKFYDzUQ66/mEqOn+iGZlb+Ynv +jtqZ8LDhzP5cfph3I3Icw6Ug+riyplOlFZ+XlPpmBVMZOp1ZSUfLy8EC5vatUudQ +NujBD++Dt+335SB3jj9dZhZ/t8MQ/Za9nWxJT49F4EIsyTQ3vUseDO/NvXhjANa3 +ZczKs/aq769yDW8qnBae7xzd9dumeLJB4rsg4uxkaa+LgeHRICaf0FG82xYY5Cmd +VKJiHwwDn0ktlmqLRKR/JBkfoHVHxDRkDxSRa/fs4xdXTFlGDkppqi6ez4KykvYX +eVWBojDfepDmx4qEAY/eOVH8CAdhkAt3ESd5k11xzTvTAgMBAAGjUzBRMB0GA1Ud +DgQWBBRFOHR5EvxMLEerfNieOFyAXRmFNDAfBgNVHSMEGDAWgBRFOHR5EvxMLEer +fNieOFyAXRmFNDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQA/ +zI6bC+MwJphriB5qTBRBFCshlTEg6QU1Sr6wOCR+ur8m4sbam9cjAWTZXNbDxU/t +IVuHWFGUSbkeXkj09BJ45iwhvH/wy3pbzMPOdp6GerZTLF239w/EIPO+2vYNxW0h +32+7Os8vIDot/NXOOfTQNkNjlxHuueVDO9BEsj+h2zIr76PYIWmhnHLMvSe1Oc3k +CgTHUw0+LDEpw+y3d1Q5NxC6QpTPpukLx+qv3FN90hmtupQd0E+oteMHfpwRDTI+ +rrKECMV4vj+cMl067yI+C/8yVJGpZx+7Z3y2h+a8hPxlWMjh5BM5jpHzfc5kwKRS +hA8GJUsKuNA1aXdXlQbZ8UdWlLCEI+DXhqsye1iWg3i5hSOwsgl+XCQF+ArkkWLR +aam0/V8ZIekTx0lqVZRYwfhUimDbBfKZh9y9ek8146fxZOsfidAJIVn6NCmznXWC +S+i7fEWaqhmDXtPXn6wx6yBtIwTPKSNy2moRvcAg8JNMJ87P33+51VMw8elcOR0g +wjGqG1YYBkhO4FE2flm14XET4Bl1ZnWbNmshySFdc5wFH5gSweMeUiRu2Tr5EHJV +Og6g4AAj7i98rLmZVOJU0bCc2onS16w5ZVx0RPTzn49/Q4pP6NTumGaNzw9p50/A +odwTrUlfLr29vdOmkrvKDl3x0a/7sK7MYvaMy1kreg== +-----END CERTIFICATE----- diff --git a/graph-oauth/src/identity/credentials/test/key.pem b/graph-oauth/src/identity/credentials/test/key.pem new file mode 100644 index 00000000..91a57263 --- /dev/null +++ b/graph-oauth/src/identity/credentials/test/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCwcckCc0RFse39 +QwuNyXHi+5QqvY1dKnIoQSIeLOGVwnwh+JbHQ1zbkXoqiJIqCV8kCMv7QLD8mPPA +z+qukOMbp5tL4dAy48Xf2wDvIFS7ss9VM5OLjlrV4shmcHJtjHooOUikR3LuTUT1 +vvvnnhEqqsjRxq59UKYSvowTYbfPj5hgtLl41LtYIE025cCQ5sAaGzdCquNAkayO +HmTKBxQ0t6eNIf9Cn7sfj77XQ0alKVCoWsX+5Xw/ZHJQAxQnryGnZgWzUbY9V/ZV +k8sTYJ022EuzkyRcYgU6s1ncZNSiLrUoQPZg9LHamva1agDEjxsoA9SxP65nSV60 +al7W3jz24YQ9ua3ezct0XPi4ChWA81EOuv5hKjp/ohmZW/mJ747amfCw4cz+XH6Y +dyNyHMOlIPq4sqZTpRWfl5T6ZgVTGTqdWUlHy8vBAub2rVLnUDbowQ/vg7ft9+Ug +d44/XWYWf7fDEP2WvZ1sSU+PReBCLMk0N71LHgzvzb14YwDWt2XMyrP2qu+vcg1v +KpwWnu8c3fXbpniyQeK7IOLsZGmvi4Hh0SAmn9BRvNsWGOQpnVSiYh8MA59JLZZq +i0SkfyQZH6B1R8Q0ZA8UkWv37OMXV0xZRg5Kaaouns+CspL2F3lVgaIw33qQ5seK +hAGP3jlR/AgHYZALdxEneZNdcc070wIDAQABAoICACHdFLkVqkq+BXEQBw2lVeg8 +ZIl3a8Qvu00igwvLjVgSxYQ3k6iWsyGo4At9vp+2wL3Hum50UgOIz293+BCc2hma +p2F+61h5Aqcd/yXlzJ1hig1OIskr/x8BsXiHqE6CIYfKxrhQxiwaRFvt1ab0XVMV +CAnK2f5PFjCOxY+Kt55sbtBQnzAjk/kr6eXZXoXw43mfM5Hl/kuMKdP03V8w0J31 +iYC6v6TnxyAdlID39n0IWuSU118+aAQ6oP3eOWYMZKceG4X47sMDONHf1Z/YcRPv +m0fRu67HWT2U4nA8Idsmn7okDaU6EVBlYWgb+h2/YXTil1WVZUGJgVWa/Ky8ZnKh +5sKEvW7dy2cnvHSxLvt/W0gLNhr69gN9hg+7aWul1Z/OLGxHu9X5rxicJQl3n9L1 +s9NV1/f37p8UYiEFOgRcTLNo0UIvGT8+1aQEdU8p2JZdNuaZdYGXiJON11Cr7SJt +FnTz3iBq0DMSChh4f7xdGK1XMhxetc0YYMMJZ0iqrDoS/FqE7YKaIvydy1JnowJN +DLfeFQmJPb0RPVy3iPwZyaOnIW/6phKz+mU+KzVt1JLRNBrOe/GF5ugPxVfpoKBV +qiveSIMpCpR0oSUVhQHreNyFqGNjPx9JGjgWUX2g2h+5Ry5w5YP8gvy4UnOdjvwb +Y26OaCv9D4Xb1bQwZEYBAoIBAQDZ0KrWsUQVKVTNHO6HM0eFyDcsUU5v408KHV9b +RjT7Z2XTDWolW8JKKdg29IyddqZyTAA4GpDDkFBY6bkZSll2SPQTvnMjnMIGk4Kt +/9FcF6p7EEfYi2cd0DjRgNPBTHVpYR8IpGcNRcB+pD0v9fmWQIdqcZlKF8qBkYOh +z5XBjUuqGN9wwMOSqqtOLK7oglPQv8fhqILhcMRklEVm11P2yxMOLz8P7UdiBQD2 +SeJiHPDgZnT7v1PvL9uUFxQTLIQxfxqCGnV6YHlUp+XREC6RkIz2+9nfDRKg4M32 +ctg0hvpbm7TGlq81d7tLEdaeQ/RCWQODyQL22RnpcMXk2h2BAoIBAQDPYHGPAuGQ +vGZobB80aL5oTu357u56VJ28YrLemyvv734kteFAvAR6HAqi0i0V7WfBEM0KvaFC +gti6rO+oGB5to2O3BFvAvaneJRJXd3wPucy1LoLnYD4hO3jWoz+pqL6NyuVwxysH +e0AelMzC6VRqobNG2t3Qfn3ewcQbjZ094IyKg50K4d/fG/ZHp/ETaMwk4aJi5Zqs +DQwghENS8DFFi9VhgmfoA6i9pecLZBtUQG6XMe8wHigWtBqnnCDJQRj5ehpoHOoR +/Hy5VZm6P+5rt3u9u6yCQjO4DOK4wWUdcj4YvJvtf5rSq70+khniusGcOM5XWflK +3QQCm/oOoStTAoIBAHOnFm70pL/PuFVInWZwVfO1AYaojUtfmKI4Ql+Gga9TkX9k +yg2YESur2EAlzVv2mh5qOFuRz3fncqIjR8Mj9SiXR/IL17r7CxLO1D5rbNDHSCAb +3uod6tyrqN+k1cr3PAh+JypBkO49MR6NOmfy5VlgXgao1sm+pCn0B76tKEkjKx/g +IRQPZkjEEj1qAF04hiR7EDjDbushI0Z8a/VVNCIAZdfMQmHEjXiAS6h6Rpft4gBk +pozdZEXGVYLmViRwKKjXYk3emq4l94Z5t88wFmn4JMEnrOGAYXnGo9XN2PrbCKgH +nw8rB1EOiAYuLJTQZCuuc+1PmCFV2SpYVNtU7QECggEAc82CfH76YS2j7kn6fvTC +51K0N66CQ5O+5bUj94UlWv4dLfjXCaQ0x8+i7Nt7S9Rf7QMpzQap549T4aKyzeZy +D31/MHNhnVcMZb3D6U4S3GE//Ck19mjbKQmh6BIPefl+N4YG7Bx8XdgorSsg0PMl +LcqQZ0/PMB+CwILptQ02spgTQ0JNzwblUhy2Qbt5twH9hdbuLFXVMqCylfHl+omg +qhp1FNeSmJB6iTT8uXK0hC7a0tmSnQcqEcuPWuunr1sHzECwQFVtLJAXQhOYHdaX +mFVE52XG5pJ7eRrQ/KUahwAFxyCD3nM0jNJUgn1psR/jAA8Ewui+elzDYYWidMrx +ewKCAQEArm8FhnisfzLaZr0tb5nC2BK8CUQiBGMIfg6MbahdG7l4kj3tKIsTUjOv +DyB0ppi6OLBPWKfoqLRuDn+iU/LyncAI3oN8OtUilLff9qFlbjC0JK/33qWW+Dif +zDwUhR747uxIgZkLHOyhKWExk/O4ZUWdGCjnOBNjMPflfdiPvb4yp+RHxthgkoGj +Bcj3nyF7rnoeG8wm1JzdtDjj2XLdAwUVgrh5Vib9VwHG05kI+cc+0sH21HjuxZbd +lyf70zHLxNbTIgeYv7WGbvNry84TcD2hF/YAuACCQImdVz7T5wBQDvOhp9f3FSR8 +ohEh/Nc/rKgvQNTtkUBBSdKksWBKRg== +-----END PRIVATE KEY----- diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs new file mode 100644 index 00000000..4dcefcda --- /dev/null +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -0,0 +1,346 @@ +use anyhow::{anyhow, Context}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use openssl::error::ErrorStack; +use openssl::hash::MessageDigest; +use openssl::pkcs12::{ParsedPkcs12_2, Pkcs12}; +use openssl::pkey::{PKey, Private}; +use openssl::rsa::Padding; +use openssl::sign::Signer; +use openssl::x509::{X509Ref, X509}; +use std::collections::HashMap; +use time::OffsetDateTime; +use uuid::Uuid; + +pub(crate) trait EncodeCert { + fn encode_cert(cert: &X509) -> anyhow::Result<String> { + Ok(format!( + "\"{}\"", + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + )) + } + + fn encode_cert_ref(cert: &X509Ref) -> anyhow::Result<String> { + Ok(format!( + "\"{}\"", + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + )) + } + + fn thumbprint(cert: &X509) -> anyhow::Result<String> { + let digest_bytes = cert + .digest(MessageDigest::sha1()) + .map_err(|err| anyhow!(err.to_string()))?; + Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) + } +} + +impl EncodeCert for X509Certificate {} + +/// Computes the client assertion used in certificate credential authorization flows. +/// The client assertion is computed from the DER encoding of an X509 certificate and it's private key. +/// +/// Client assertions are generated using the openssl library for security reasons. +/// You can see an example of how this is done by Microsoft located at +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions +pub struct X509Certificate { + client_id: String, + tenant_id: Option<String>, + claims: Option<HashMap<String, String>>, + extend_claims: bool, + certificate: X509, + pkey: PKey<Private>, + certificate_chain: bool, + parsed_pkcs12: Option<ParsedPkcs12_2>, + uuid: Uuid, +} + +impl X509Certificate { + pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { + Self { + client_id: client_id.as_ref().to_owned(), + tenant_id: None, + claims: None, + extend_claims: true, + certificate, + certificate_chain: false, + pkey: private_key, + parsed_pkcs12: None, + uuid: Uuid::new_v4(), + } + } + + pub fn new_from_pass<T: AsRef<str>>( + client_id: T, + pass: T, + certificate: X509, + ) -> anyhow::Result<Self> { + let der = X509Certificate::encode_cert(&certificate)?; + let parsed_pkcs12 = + Pkcs12::from_der(&URL_SAFE_NO_PAD.decode(der)?)?.parse2(pass.as_ref())?; + + let _ = parsed_pkcs12.cert.as_ref().ok_or(anyhow::Error::msg( + "No certificate found after parsing Pkcs12 using pass", + ))?; + + let private_key = parsed_pkcs12.pkey.as_ref().ok_or(anyhow::Error::msg( + "No private key found after parsing Pkcs12 using pass", + ))?; + + Ok(Self { + client_id: client_id.as_ref().to_owned(), + tenant_id: None, + claims: None, + extend_claims: true, + certificate, + certificate_chain: true, + pkey: private_key.clone(), + parsed_pkcs12: Some(parsed_pkcs12), + uuid: Uuid::new_v4(), + }) + } + + pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) { + self.tenant_id = Some(tenant.as_ref().to_owned()); + } + + /// Provide your own set of claims in the payload of the JWT. + /// + /// Replace the claims that would be generated for the client assertion. + /// This replaces the following payload fields: aud, exp, nbf, jti, sub, and iss. + /// Only the claims given are passed for the payload of the JWT used in the client assertion. + pub fn replace_claims(&mut self, claims: HashMap<String, String>) { + self.claims = Some(claims); + self.extend_claims = false; + } + + /// Provide your own set of claims in the payload of the JWT. + /// + /// Set extend_claims to false in order to replace the claims that would be generated + /// for the client assertion. This replaces the following payload fields: aud, exp, nbf, jti, + /// sub, and iss. This ensures that only the claims given are passed for the payload of the JWT + /// used in the client assertion. + /// + /// If extend claims is true, the claims provided are in addition + /// to those claims mentioned above and do not replace them, however, any claim provided + /// with the same fields above will replace those that are generated. + pub fn extend_claims(&mut self, claims: HashMap<String, String>) { + match self.claims.as_mut() { + Some(c) => c.extend(claims), + None => self.claims = Some(claims), + } + + self.extend_claims = true; + } + + /// Hex encoded SHA-1 thumbprint of the X.509 certificate's DER encoding. + /// + /// You can verify that the correct certificate has been passed + /// by comparing the hex encoded thumbprint against the thumbprint given in Azure + /// Active Directory under Certificates and Secrets for your application or by looking + /// at the keyCredentials customKeyIdentifier field in your applications manifest. + pub fn get_hex_thumbprint(&self) -> Result<String, ErrorStack> { + let digest_bytes = self.certificate.digest(MessageDigest::sha1())?; + Ok(hex::encode(digest_bytes.as_ref()).to_uppercase()) + } + + /// Base64 Url encoded (No Pad) SHA-1 thumbprint of the X.509 certificate's DER encoding. + pub fn get_thumbprint(&self) -> anyhow::Result<String> { + let digest_bytes = self + .certificate + .digest(MessageDigest::sha1()) + .map_err(|err| anyhow!(err.to_string()))?; + Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) + } + + /// Get the value used for the jti field in the payload. This field is computed + /// when constructing the [X509Certificate] and will be different from any + /// custom claims provided. + /// + /// The "jti" (JWT ID) claim provides a unique identifier for the JWT. + /// The identifier value MUST be assigned in a manner that ensures that there is + /// a negligible probability that the same value will be accidentally assigned to + /// a different data object; if the application uses multiple issuers, collisions + /// MUST be prevented among values produced by different issuers as well. + pub fn get_uuid(&self) -> &Uuid { + &self.uuid + } + + /// Set the UUID for the jti field of the claims/payload of the jwt. + pub fn set_uuid(&mut self, value: Uuid) { + self.uuid = value; + } + + fn x5c(&self) -> anyhow::Result<String> { + let parsed_pkcs12 = self.parsed_pkcs12.as_ref().ok_or(anyhow!( + "No certificate found after parsing Pkcs12 using pass" + ))?; + + let certificate = parsed_pkcs12.cert.as_ref().ok_or(anyhow!( + "No certificate found after parsing Pkcs12 using pass" + ))?; + + let sig = X509Certificate::encode_cert(certificate)?; + + if let Some(stack) = parsed_pkcs12.ca.as_ref() { + let chain = stack + .into_iter() + .map(X509Certificate::encode_cert_ref) + .collect::<anyhow::Result<Vec<String>>>() + .map_err(|err| { + anyhow!("Unable to encode certificates in certificate chain - error {err}") + })? + .join(","); + + Ok(format! {"{},{}", sig, chain}) + } else { + Ok(sig) + } + } + + fn get_header(&self) -> anyhow::Result<HashMap<String, String>> { + let mut header = HashMap::new(); + header.insert("x5t".to_owned(), self.get_thumbprint()?); + header.insert("alg".to_owned(), "RS256".to_owned()); + header.insert("typ".to_owned(), "JWT".to_owned()); + + if self.certificate_chain && self.parsed_pkcs12.is_some() { + let x5c = self.x5c()?; + header.insert("x5c".to_owned(), x5c); + } + + Ok(header) + } + + fn get_claims(&self, tenant_id: Option<String>) -> anyhow::Result<HashMap<String, String>> { + if let Some(claims) = self.claims.as_ref() { + if !self.extend_claims { + return Ok(claims.clone()); + } + } + + let aud = { + if let Some(tenant_id) = tenant_id.as_ref() { + format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + tenant_id + ) + } else if let Some(tenant_id) = self.tenant_id.as_ref() { + format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0/token", + tenant_id + ) + } else { + "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_owned() + } + }; + + // 10 minutes until expiration. + let exp = 60 * 10; + let nbf = OffsetDateTime::now_utc().unix_timestamp(); + let exp = nbf + exp; + + let mut claims = HashMap::new(); + claims.insert("aud".to_owned(), aud); + claims.insert("exp".to_owned(), exp.to_string()); + claims.insert("nbf".to_owned(), nbf.to_string()); + claims.insert("jti".to_owned(), self.uuid.to_string()); + claims.insert("sub".to_owned(), self.client_id.to_owned()); + claims.insert("iss".to_owned(), self.client_id.to_owned()); + + if let Some(internal_claims) = self.claims.as_ref() { + claims.extend(internal_claims.clone()); + } + + Ok(claims) + } + + /// JWT Header and Payload in the format header.payload + fn base64_token(&self, tenant_id: Option<String>) -> anyhow::Result<String> { + let header = self.get_header()?; + let header = serde_json::to_string(&header)?; + let header_base64 = URL_SAFE_NO_PAD.encode(header.as_bytes()); + + let claims = self.get_claims(tenant_id)?; + let claims = serde_json::to_string(&claims)?; + let claims_base64 = URL_SAFE_NO_PAD.encode(claims.as_bytes()); + + Ok(format!("{}.{}", header_base64, claims_base64)) + } + + /* + Altogether the general flow is as follows: + + let header = self.get_header()?; + let header = serde_json::to_string(&header).unwrap(); + let header_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(header.as_bytes()); + let claims = self.get_claims(); + let claims = serde_json::to_string(&claims).unwrap(); + let claims_base64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.as_bytes()); + let token = format!("{}.{}", header_base64, claims_base64); + + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; + signer.set_rsa_padding(Padding::PKCS1)?; + signer.update(token.as_str().as_bytes())?; + let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + let signed_client_assertion = format!("{token}.{signature}"); + Ok(signed_client_assertion) + */ + + /// Get the signed client assertion. + /// + /// The signature is a Base64 Url encoded (No Pad) JWT Header and Payload signed with the private key using SHA_256 + /// and RSA padding PKCS1 + pub fn sign(&self, tenant_id: Option<String>) -> anyhow::Result<String> { + let token = self.base64_token(tenant_id)?; + + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; + signer.set_rsa_padding(Padding::PKCS1)?; + signer.update(token.as_str().as_bytes())?; + let signature = URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + + Ok(format!("{token}.{signature}")) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn claims() { + let cert_bytes = include_bytes!("test/cert.pem"); + let private_key_bytes = include_bytes!("test/key.pem"); + + let cert = X509::from_pem(cert_bytes).unwrap(); + let private_key = PKey::private_key_from_pem(private_key_bytes).unwrap(); + + let mut certificate = X509Certificate::new("client_id", cert, private_key); + assert!(certificate.claims.is_none()); + + let mut claims = HashMap::new(); + claims.insert("c".to_string(), "fake claim".to_string()); + certificate.extend_claims(claims); + + let extended_claims = certificate.get_claims(None).unwrap(); + assert!(extended_claims.contains_key("iss")); + assert!(extended_claims.contains_key("sub")); + assert_eq!( + extended_claims.get("aud").unwrap().as_str(), + "https://login.microsoftonline.com/common/oauth2/v2.0/token" + ); + assert_eq!(extended_claims.get("c").unwrap().as_str(), "fake claim"); + } + + #[test] + pub fn sign() { + let cert_bytes = include_bytes!("test/cert.pem"); + let private_key_bytes = include_bytes!("test/key.pem"); + + let cert = X509::from_pem(cert_bytes).unwrap(); + let private_key = PKey::private_key_from_pem(private_key_bytes).unwrap(); + + let certificate = X509Certificate::new("client_id", cert, private_key); + assert!(certificate.sign(None).is_ok()); + } +} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 58278d87..3c70bc18 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -29,7 +29,7 @@ //! //! # Example //! ``` -//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; +//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication, CredentialBuilder}; //! //! pub fn authorization_url(client_id: &str) { //! let _url = AuthorizationCodeCredential::authorization_url_builder() @@ -40,16 +40,16 @@ //! .unwrap(); //! } //! -//! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> ConfidentialClientApplication { +//! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication> { //! let credential = AuthorizationCodeCredential::builder() //! .with_authorization_code(authorization_code) //! .with_client_id(client_id) //! .with_client_secret(client_secret) //! .with_scope(vec!["user.read"]) -//! .with_redirect_uri("http://localhost:8000/redirect") +//! .with_redirect_uri("http://localhost:8000/redirect")? //! .build(); //! -//! ConfidentialClientApplication::from(credential) +//! Ok(ConfidentialClientApplication::from(credential)) //! } //! ``` From c873cbe29ea4ff8f8e8efb28838b97b78f57be4f Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 21 May 2023 17:21:47 -0400 Subject: [PATCH 019/118] Update credential serialization and add OpenIdCredential --- .cargo/config.toml | 2 +- graph-error/src/authorization_failure.rs | 20 +- graph-http/.cargo/config.toml | 2 + graph-http/src/client.rs | 53 +-- graph-http/src/lib.rs | 3 +- graph-http/src/traits/api_client_impl.rs | 28 ++ graph-http/src/traits/mod.rs | 2 + graph-oauth/.cargo/config.toml | 2 + graph-oauth/src/auth.rs | 138 +++++--- graph-oauth/src/auth_response_query.rs | 4 +- graph-oauth/src/identity/authority.rs | 23 +- .../src/identity/credentials/as_query.rs | 3 + .../auth_code_authorization_url.rs | 125 ++++--- ...thorization_code_certificate_credential.rs | 78 +++-- .../authorization_code_credential.rs | 104 +++--- .../client_certificate_credential.rs | 56 ++-- .../client_credentials_authorization_url.rs | 10 +- .../credentials/client_secret_credential.rs | 13 +- .../code_flow_authorization_url.rs | 24 +- .../credentials/code_flow_credential.rs | 78 ++--- .../src/identity/credentials/crypto.rs | 10 +- .../credentials/device_code_credential.rs | 65 ++-- .../implicit_credential_authorization_url.rs | 54 ++-- graph-oauth/src/identity/credentials/mod.rs | 2 + .../credentials/open_id_authorization_url.rs | 135 ++++++-- .../credentials/open_id_credential.rs | 305 +++++++++++++++++- .../src/identity/credentials/prompt.rs | 21 ++ .../proof_key_for_code_exchange.rs | 14 +- .../resource_owner_password_credential.rs | 22 +- .../token_flow_authorization_url.rs | 23 +- .../identity/credentials/x509_certificate.rs | 2 +- graph-oauth/src/identity/form_credential.rs | 6 +- test-tools/.cargo/config.toml | 2 + 33 files changed, 959 insertions(+), 470 deletions(-) create mode 100644 graph-http/.cargo/config.toml create mode 100644 graph-http/src/traits/api_client_impl.rs create mode 100644 graph-oauth/.cargo/config.toml create mode 100644 graph-oauth/src/identity/credentials/as_query.rs create mode 100644 test-tools/.cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml index a41675fd..4ae96a70 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [env] -GRAPH_RS_SDK = "1.1.1" +USER_AGENT = "graph-rs-sdk/1.1.1" diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 14fef70e..21050970 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,5 +1,7 @@ use crate::AuthorizationResult; +pub type AF = AuthorizationFailure; + #[derive(Debug, thiserror::Error)] pub enum AuthorizationFailure { #[error("Required value missing:\n{0:#?}", name)] @@ -13,34 +15,34 @@ pub enum AuthorizationFailure { } impl AuthorizationFailure { - pub fn required_value<T: AsRef<str>>(name: T) -> AuthorizationFailure { + pub fn err<T: AsRef<str>>(name: T) -> AuthorizationFailure { AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), message: None, } } - pub fn required_value_result<T: AsRef<str>, U>(name: T) -> AuthorizationResult<U> { + pub fn result<U>(name: impl AsRef<str>) -> AuthorizationResult<U> { Err(AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), message: None, }) } - pub fn required_value_msg<T: AsRef<str>>(name: T, message: Option<T>) -> AuthorizationFailure { + pub fn msg_err<T: AsRef<str>>(name: T, message: T) -> AuthorizationFailure { AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), - message: message.map(|s| s.as_ref().to_owned()), + message: Some(message.as_ref().to_owned()), } } - pub fn required_value_msg_result<T>( - name: &str, - message: Option<&str>, + pub fn msg_result<T>( + name: impl AsRef<str>, + message: impl ToString, ) -> Result<T, AuthorizationFailure> { Err(AuthorizationFailure::RequiredValue { - name: name.to_owned(), - message: message.map(|s| s.to_owned()), + name: name.as_ref().to_owned(), + message: Some(message.to_string()), }) } diff --git a/graph-http/.cargo/config.toml b/graph-http/.cargo/config.toml new file mode 100644 index 00000000..a41675fd --- /dev/null +++ b/graph-http/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +GRAPH_RS_SDK = "1.1.1" diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index a4580c71..477186fb 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,7 +1,4 @@ use crate::blocking::BlockingClient; -use crate::traits::ODataQuery; - -use graph_error::GraphResult; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -9,7 +6,6 @@ use std::env::VarError; use std::ffi::OsStr; use std::fmt::{Debug, Formatter}; use std::time::Duration; -use url::Url; #[derive(Clone)] struct ClientConfiguration { @@ -145,12 +141,12 @@ impl GraphClientConfiguration { if !self.config.headers.contains_key(USER_AGENT) { let mut headers = self.config.headers.clone(); - let version = std::env::var("GRAPH_RS_SDK").unwrap(); - headers.insert( - USER_AGENT, - HeaderValue::from_str(&format!("graph-rs-sdk/{version}")).unwrap(), - ); - builder = builder.default_headers(self.config.headers); + if let Ok(user_agent_header) = std::env::var("USER_AGENT") { + if let Ok(header_value) = HeaderValue::from_str(&user_agent_header) { + headers.insert(USER_AGENT, header_value); + builder = builder.default_headers(self.config.headers); + } + } } else { builder = builder.default_headers(self.config.headers); } @@ -182,12 +178,12 @@ impl GraphClientConfiguration { if !self.config.headers.contains_key(USER_AGENT) { let mut headers = self.config.headers.clone(); - let version = std::env::var("GRAPH_RS_SDK").unwrap(); - headers.insert( - USER_AGENT, - HeaderValue::from_str(&format!("graph-rs-sdk/{version}")).unwrap(), - ); - builder = builder.default_headers(self.config.headers); + if let Ok(user_agent_header) = std::env::var("USER_AGENT") { + if let Ok(header_value) = HeaderValue::from_str(&user_agent_header) { + headers.insert(USER_AGENT, header_value); + builder = builder.default_headers(self.config.headers); + } + } } else { builder = builder.default_headers(self.config.headers); } @@ -261,28 +257,3 @@ impl Debug for Client { .finish() } } - -pub trait ApiClientImpl: ODataQuery + Sized { - fn url(&self) -> Url; - - fn render_path<S: AsRef<str>>( - &self, - path: S, - path_params_map: &serde_json::Value, - ) -> GraphResult<String>; - - fn build_url<S: AsRef<str>>( - &self, - path: S, - path_params_map: &serde_json::Value, - ) -> GraphResult<Url> { - let path = self.render_path(path.as_ref(), path_params_map)?; - let mut vec: Vec<&str> = path.split('/').collect(); - vec.retain(|s| !s.is_empty()); - let mut url = self.url(); - if let Ok(mut p) = url.path_segments_mut() { - p.extend(&vec); - } - Ok(url) - } -} diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index faa707f6..93c5ffd0 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -37,8 +37,7 @@ pub mod api_impl { pub use crate::request_components::RequestComponents; pub use crate::request_handler::RequestHandler; pub use crate::resource_identifier::{ResourceConfig, ResourceIdentifier}; - pub use crate::traits::BodyExt; - pub use crate::traits::ODataQuery; + pub use crate::traits::{ApiClientImpl, BodyExt, ODataQuery}; pub use crate::upload_session::UploadSession; pub use graph_error::{GraphFailure, GraphResult}; } diff --git a/graph-http/src/traits/api_client_impl.rs b/graph-http/src/traits/api_client_impl.rs new file mode 100644 index 00000000..75d3322d --- /dev/null +++ b/graph-http/src/traits/api_client_impl.rs @@ -0,0 +1,28 @@ +use crate::api_impl::ODataQuery; +use graph_error::GraphResult; +use url::Url; + +pub trait ApiClientImpl: ODataQuery + Sized { + fn url(&self) -> Url; + + fn render_path<S: AsRef<str>>( + &self, + path: S, + path_params_map: &serde_json::Value, + ) -> GraphResult<String>; + + fn build_url<S: AsRef<str>>( + &self, + path: S, + path_params_map: &serde_json::Value, + ) -> GraphResult<Url> { + let path = self.render_path(path.as_ref(), path_params_map)?; + let mut vec: Vec<&str> = path.split('/').collect(); + vec.retain(|s| !s.is_empty()); + let mut url = self.url(); + if let Ok(mut p) = url.path_segments_mut() { + p.extend(&vec); + } + Ok(url) + } +} diff --git a/graph-http/src/traits/mod.rs b/graph-http/src/traits/mod.rs index 02f527e7..47cb3eb2 100644 --- a/graph-http/src/traits/mod.rs +++ b/graph-http/src/traits/mod.rs @@ -1,3 +1,4 @@ +mod api_client_impl; mod async_iterator; mod async_try_from; mod body_ext; @@ -8,6 +9,7 @@ mod odata_query; mod response_blocking_ext; mod response_ext; +pub use api_client_impl::*; pub use async_iterator::*; pub use async_try_from::*; pub use body_ext::*; diff --git a/graph-oauth/.cargo/config.toml b/graph-oauth/.cargo/config.toml new file mode 100644 index 00000000..a41675fd --- /dev/null +++ b/graph-oauth/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +GRAPH_RS_SDK = "1.1.1" diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index cf700850..2be3be2f 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,7 +1,7 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; -use crate::identity::form_credential::SerializerField; +use crate::identity::form_credential::ParameterIs; use crate::identity::{Authority, AzureAuthorityHost}; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; @@ -151,6 +151,7 @@ pub struct OAuthSerializer { access_token: Option<AccessToken>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, + parameters: HashMap<ParameterIs, String>, } impl OAuthSerializer { @@ -167,6 +168,7 @@ impl OAuthSerializer { access_token: None, scopes: BTreeSet::new(), credentials: BTreeMap::new(), + parameters: HashMap::with_capacity(10), } } @@ -812,7 +814,7 @@ impl OAuthSerializer { pub fn join_scopes(&self, sep: &str) -> String { self.scopes .iter() - .map(|s| s.as_str()) + .map(|s| &**s) .collect::<Vec<&str>>() .join(sep) } @@ -825,8 +827,8 @@ impl OAuthSerializer { /// # use std::collections::HashSet; /// # let mut oauth = OAuthSerializer::new(); /// - /// let scopes1 = vec!["Files.Read", "Files.ReadWrite"]; - /// oauth.extend_scopes(&scopes1); + /// let scopes = vec!["Files.Read", "Files.ReadWrite"]; + /// oauth.extend_scopes(&scopes); /// /// assert_eq!(oauth.join_scopes(" "), "Files.Read Files.ReadWrite"); /// ``` @@ -1024,6 +1026,21 @@ impl OAuthSerializer { self.get(c).ok_or_else(|| OAuthError::credential_error(c)) } + pub fn ok_or(&self, oac: &OAuthParameter) -> AuthorizationResult<String> { + self.get(*oac).ok_or(AuthorizationFailure::err(oac)) + } + + pub fn try_as_tuple(&self, oac: &OAuthParameter) -> AuthorizationResult<(String, String)> { + if oac.eq(&OAuthParameter::Scope) { + Ok((oac.alias().to_owned(), self.join_scopes(" "))) + } else { + Ok(( + oac.alias().to_owned(), + self.get(*oac).ok_or(AuthorizationFailure::err(oac))?, + )) + } + } + pub fn form_encode_credentials( &mut self, pairs: Vec<OAuthParameter>, @@ -1041,49 +1058,64 @@ impl OAuthSerializer { }); } - fn query_encode_filter(&self, form_credential: &SerializerField) -> bool { - let oac = { - match form_credential { - SerializerField::Required(oac) => *oac, - SerializerField::NotRequired(oac) => *oac, + pub fn encode_query( + &mut self, + optional_fields: Vec<OAuthParameter>, + required_fields: Vec<OAuthParameter>, + encoder: &mut Serializer<String>, + ) -> AuthorizationResult<()> { + for parameter in required_fields { + if parameter.alias().eq("scope") { + if self.scopes.is_empty() { + return AuthorizationFailure::result::<()>(parameter.alias()); + } else { + encoder.append_pair("scope", self.join_scopes(" ").as_str()); + } + } else { + let value = self + .get(parameter) + .ok_or(AuthorizationFailure::err(parameter))?; + + encoder.append_pair(parameter.alias(), value.as_str()); } - }; - self.contains_key(oac.alias()) || oac.alias().eq("scope") + } + + for parameter in optional_fields { + if parameter.alias().eq("scope") && !self.scopes.is_empty() { + encoder.append_pair("scope", self.join_scopes(" ").as_str()); + } else if let Some(val) = self.get(parameter) { + encoder.append_pair(parameter.alias(), val.as_str()); + } + } + + Ok(()) } pub fn url_query_encode( &mut self, - pairs: Vec<SerializerField>, + pairs: Vec<ParameterIs>, encoder: &mut Serializer<String>, ) -> AuthorizationResult<()> { for form_credential in pairs.iter() { - if self.query_encode_filter(form_credential) { - match form_credential { - SerializerField::Required(oac) => { - if oac.alias().eq("scope") { - if self.scopes.is_empty() { - return AuthorizationFailure::required_value_msg_result::<()>( - oac.alias(), - None, - ); - } else { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); - } - } else if let Some(val) = self.get(*oac) { - encoder.append_pair(oac.alias(), val.as_str()); + match form_credential { + ParameterIs::Required(oac) => { + if oac.alias().eq("scope") { + if self.scopes.is_empty() { + return AuthorizationFailure::result::<()>(oac.alias()); } else { - return AuthorizationFailure::required_value_msg_result::<()>( - oac.alias(), - None, - ); - } - } - SerializerField::NotRequired(oac) => { - if oac.alias().eq("scope") && !self.scopes.is_empty() { encoder.append_pair("scope", self.join_scopes(" ").as_str()); - } else if let Some(val) = self.get(*oac) { - encoder.append_pair(oac.alias(), val.as_str()); } + } else { + let value = self.get(*oac).ok_or(AuthorizationFailure::err(oac))?; + + encoder.append_pair(oac.alias(), value.as_str()); + } + } + ParameterIs::Optional(oac) => { + if oac.alias().eq("scope") && !self.scopes.is_empty() { + encoder.append_pair("scope", self.join_scopes(" ").as_str()); + } else if let Some(val) = self.get(*oac) { + encoder.append_pair(oac.alias(), val.as_str()); } } } @@ -1110,26 +1142,44 @@ impl OAuthSerializer { Ok(map) } + pub fn as_credential_map( + &mut self, + optional_fields: Vec<OAuthParameter>, + required_fields: Vec<OAuthParameter>, + ) -> AuthorizationResult<HashMap<String, String>> { + let mut required_map = required_fields + .iter() + .map(|oac| self.try_as_tuple(oac)) + .collect::<AuthorizationResult<HashMap<String, String>>>()?; + + let optional_map: HashMap<String, String> = optional_fields + .iter() + .flat_map(|oac| self.try_as_tuple(oac)) + .collect(); + + required_map.extend(optional_map); + Ok(required_map) + } + pub fn authorization_form( &mut self, - form_credentials: Vec<SerializerField>, + form_credentials: Vec<ParameterIs>, ) -> AuthorizationResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for form_credential in form_credentials.iter() { match form_credential { - SerializerField::Required(oac) => { - let val = self.get(*oac).ok_or(AuthorizationFailure::RequiredValue { - name: oac.alias().into(), - message: None, - })?; + ParameterIs::Required(oac) => { + let val = self + .get(*oac) + .ok_or(AuthorizationFailure::err(oac.alias()))?; if val.trim().is_empty() { - return AuthorizationFailure::required_value_result(oac); + return AuthorizationFailure::msg_result(oac, "Value cannot be empty"); } else { map.insert(oac.to_string(), val); } } - SerializerField::NotRequired(oac) => { + ParameterIs::Optional(oac) => { if oac.eq(&OAuthParameter::Scope) && !self.scopes.is_empty() { map.insert("scope".into(), self.join_scopes(" ")); } else if let Some(val) = self.get(*oac) { diff --git a/graph-oauth/src/auth_response_query.rs b/graph-oauth/src/auth_response_query.rs index d524f8b1..307a32cd 100644 --- a/graph-oauth/src/auth_response_query.rs +++ b/graph-oauth/src/auth_response_query.rs @@ -9,8 +9,10 @@ pub struct AuthQueryResponse { pub access_token: Option<String>, pub state: Option<String>, pub nonce: Option<String>, + pub error: Option<String>, + pub error_description: Option<String>, #[serde(flatten)] - additional_fields: HashMap<String, Value>, + pub additional_fields: HashMap<String, Value>, #[serde(skip)] log_pii: bool, } diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 560b1bc4..4c2aa2b7 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -62,13 +62,34 @@ impl AzureAuthorityHost { #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Authority { + /// Users with both a personal Microsoft account and a work or school account + /// from Azure AD can sign in to the application. + /// + /// [Authority::AzureActiveDirectory] is the same as [Authority::Common]. + /// [Authority::Common] is a convenience enum variant that may be more + /// familiar with it from the Microsoft Identity Platform documentation. #[default] AzureActiveDirectory, AzureDirectoryFederatedServices, - /// Same as Aad. This is here since `common` is more familiar some times + /// Users with both a personal Microsoft account and a work or school account + /// from Azure AD can sign in to the application. + /// + /// [Authority::Common] is the same as [Authority::AzureActiveDirectory]. + /// + /// [Authority::Common] is a convenience enum variant that may be more + /// familiar with it from the Microsoft Identity Platform documentation. Common, + /// Only users with work or school accounts from Azure AD can sign in to the application. Organizations, + /// Only users with a personal Microsoft account can sign in to the application. Consumers, + /// The value can be the domain name of the Azure AD tenant or the tenant ID in GUID format. + /// You can also use the consumer tenant GUID, 9188040d-6c67-4c5b-b112-36a304b66dad, + /// in place of consumers. + /// + /// Only users from a specific Azure AD tenant (directory members with a work or + /// school account or directory guests with a personal Microsoft account) can sign in + /// to the application. TenantId(String), } diff --git a/graph-oauth/src/identity/credentials/as_query.rs b/graph-oauth/src/identity/credentials/as_query.rs new file mode 100644 index 00000000..ecdd897e --- /dev/null +++ b/graph-oauth/src/identity/credentials/as_query.rs @@ -0,0 +1,3 @@ +pub trait AsQuery<RHS = Self> { + fn as_query(&self) -> String; +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index d58d930a..1089eded 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -3,11 +3,10 @@ use crate::auth_response_query::AuthQueryResponse; use crate::identity::{ Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, }; -use crate::oauth::form_credential::SerializerField; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; use crate::web::{InteractiveAuthenticator, InteractiveWebViewOptions}; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; @@ -92,6 +91,16 @@ impl AuthCodeAuthorizationUrl { self.authorization_url_with_host(azure_authority_host) } + /// Get the nonce. + /// + /// This value may be generated automatically by the client and may be useful for users + /// who want to manually verify that the nonce stored in the client is the same as the + /// nonce returned in the response from the authorization server. + /// Verifying the nonce helps mitigate token replay attacks. + pub fn nonce(&mut self) -> Option<&String> { + self.nonce.as_ref() + } + pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, @@ -126,11 +135,9 @@ impl AuthCodeAuthorizationUrl { */ let url = Url::parse(&url_string)?; - let query = url.query().ok_or(AuthorizationFailure::required_value_msg( + let query = url.query().ok_or(AF::msg_err( "query", - Some(&format!( - "Url returned on redirect is missing query parameters, url: {url}" - )), + &format!("Url returned on redirect is missing query parameters, url: {url}"), ))?; let response_query: AuthQueryResponse = serde_urlencoded::from_str(query)?; @@ -147,16 +154,16 @@ mod web_view_authenticator { &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, ) -> anyhow::Result<Option<String>> { - let url = self.authorization_url()?; - let redirect_url = self.redirect_uri()?; + let uri = self.authorization_url()?; + let redirect_uri = self.redirect_uri()?; let web_view_options = interactive_web_view_options.unwrap_or_default(); let _timeout = web_view_options.timeout; let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { InteractiveWebView::interactive_authentication( - url, - redirect_url, + uri, + redirect_uri, web_view_options, sender, ) @@ -190,15 +197,22 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("redirect_uri", None); + return AuthorizationFailure::result("redirect_uri"); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("client_id", None); + return AuthorizationFailure::result("client_id"); } if self.scope.is_empty() { - return AuthorizationFailure::required_value_msg_result("scope", None); + return AuthorizationFailure::result("scope"); + } + + if self.scope.contains(&String::from("openid")) { + return AuthorizationFailure::msg_result( + "openid", + "Scope openid is not valid for authorization code - instead use OpenIdCredential", + ); } serializer @@ -227,9 +241,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { if self.response_type.contains(&ResponseType::IdToken) { if let Some(response_mode) = self.response_mode.as_ref() { // id_token requires fragment or form_post. The Microsoft identity - // platform recommends form_post. Unless you explicitly set - // fragment then form_post is used here. Please file an issue - // if you experience encounter related problems. + // platform recommends form_post but fragment is default. if response_mode.eq(&ResponseMode::Query) { serializer.response_mode(ResponseMode::Fragment.as_ref()); } else { @@ -271,34 +283,33 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { serializer.code_challenge_method(code_challenge_method.as_str()); } - let authorization_credentials = vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ResponseType), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::NotRequired(OAuthParameter::ResponseMode), - SerializerField::NotRequired(OAuthParameter::State), - SerializerField::NotRequired(OAuthParameter::Prompt), - SerializerField::NotRequired(OAuthParameter::LoginHint), - SerializerField::NotRequired(OAuthParameter::DomainHint), - SerializerField::NotRequired(OAuthParameter::Nonce), - SerializerField::NotRequired(OAuthParameter::CodeChallenge), - SerializerField::NotRequired(OAuthParameter::CodeChallengeMethod), - ]; - let mut encoder = Serializer::new(String::new()); - serializer.url_query_encode(authorization_credentials, &mut encoder)?; - - if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); - Ok(url) - } else { - AuthorizationFailure::required_value_msg_result( - "authorization_url", - Some("Internal Error"), - ) - } + serializer.encode_query( + vec![ + OAuthParameter::ResponseMode, + OAuthParameter::State, + OAuthParameter::Prompt, + OAuthParameter::LoginHint, + OAuthParameter::DomainHint, + OAuthParameter::Nonce, + OAuthParameter::CodeChallenge, + OAuthParameter::CodeChallengeMethod, + ], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ResponseType, + OAuthParameter::RedirectUri, + OAuthParameter::Scope, + ], + &mut encoder, + )?; + + let authorization_url = serializer + .get(OAuthParameter::AuthorizationUrl) + .ok_or(AF::msg_err("authorization_url", "Internal Error"))?; + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) } } @@ -363,7 +374,9 @@ impl AuthCodeAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.auth_url_parameters.response_type = response_type.into_iter().collect(); + self.auth_url_parameters + .response_type + .extend(response_type.into_iter()); self } @@ -403,8 +416,9 @@ impl AuthCodeAuthorizationUrlBuilder { /// generate a secure random 32-octet sequence that is base64 URL /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.nonce = Some(Crypto::secure_random_string()?); + #[doc(hidden)] + pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { + self.auth_url_parameters.nonce = Some(Crypto::sha256_secure_string()?.1); Ok(self) } @@ -413,8 +427,18 @@ impl AuthCodeAuthorizationUrlBuilder { self } - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.auth_url_parameters.scope = scopes.into_iter().map(|s| s.to_string()).collect(); + /// Set the required permissions for the authorization request. + /// + /// Providing a scope of `id_token` automatically adds [ResponseType::IdToken] + /// and generates a secure nonce value. + /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.auth_url_parameters.scope.extend( + scope + .into_iter() + .map(|s| s.to_string()) + .map(|s| s.trim().to_owned()), + ); if self.auth_url_parameters.nonce.is_none() && self @@ -456,7 +480,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// Including `id_token` also requires a nonce parameter. /// This is generated automatically. /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] - pub fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { + fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; self.auth_url_parameters .response_type @@ -573,8 +597,9 @@ mod test { .unwrap(); let query = url.query().unwrap(); + dbg!(query); assert!(query.contains("response_mode=fragment")); - assert!(query.contains("response_type=id_token")); + assert!(query.contains("response_type=code+id_token")); } #[test] diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 261fe536..fe431232 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,12 +1,11 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, TokenCredentialOptions, TokenRequest, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationResult, AF}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; @@ -109,30 +108,24 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( - OAuthParameter::ClientId.alias(), - None, - ); + return AF::result(OAuthParameter::ClientId); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( - OAuthParameter::ClientAssertion.alias(), - None, - ); + return AF::result(OAuthParameter::ClientAssertion); } if self.client_assertion_type.trim().is_empty() { - self.client_assertion_type = - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".to_owned(); + self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } self.serializer @@ -148,9 +141,9 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AF::msg_result( OAuthParameter::RefreshToken.alias(), - Some("refresh_token is an empty string"), + "refresh_token is empty - cannot be an empty string", ); } @@ -158,19 +151,21 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { .refresh_token(refresh_token.as_ref()) .grant_type("refresh_token"); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::RefreshToken), - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::Required(OAuthParameter::ClientAssertion), - SerializerField::Required(OAuthParameter::ClientAssertionType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]); + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::RefreshToken, + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AF::msg_result( OAuthParameter::AuthorizationCode.alias(), - Some("authorization_code is an empty string"), + "authorization_code is empty - cannot be an empty string", ); } @@ -178,25 +173,26 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { .authorization_code(authorization_code.as_str()) .grant_type("authorization_code"); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::AuthorizationCode), - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::Required(OAuthParameter::ClientAssertion), - SerializerField::Required(OAuthParameter::ClientAssertionType), - SerializerField::NotRequired(OAuthParameter::Scope), - SerializerField::NotRequired(OAuthParameter::CodeVerifier), - ]); + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![ + OAuthParameter::AuthorizationCode, + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::RedirectUri, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ); } - AuthorizationFailure::required_value_msg_result( - &format!( + AF::msg_result( + format!( "{} or {}", OAuthParameter::AuthorizationCode.alias(), OAuthParameter::RefreshToken.alias() ), - Some("Either authorization code or refresh token is required"), + "Either authorization code or refresh token is required", ) } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index ae8ccf8b..60f7fb8c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,12 +1,11 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, }; use crate::oauth::AuthCodeAuthorizationUrlBuilder; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationResult, AF}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; @@ -114,33 +113,27 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "access_token_url", - Some("Internal Error"), - ), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) } else { - let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "refresh_token_url", - Some("Internal Error"), - ), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + let uri = self + .serializer + .get(OAuthParameter::RefreshTokenUrl) + .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) } } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AF::result(OAuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_result( - OAuthParameter::ClientSecret.alias(), - ); + return AF::result(OAuthParameter::ClientSecret.alias()); } self.serializer @@ -150,28 +143,27 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( - OAuthParameter::RefreshToken.alias(), - Some("Either authorization code or refresh token is required"), - ); + return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); } self.serializer .grant_type("refresh_token") .refresh_token(refresh_token.as_ref()); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ClientSecret), - SerializerField::Required(OAuthParameter::RefreshToken), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]); + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RefreshToken, + OAuthParameter::GrantType, + ], + ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AF::msg_result( OAuthParameter::AuthorizationCode.alias(), - Some("Either authorization code or refresh token is required"), + "Authorization code is empty", ); } @@ -184,24 +176,25 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { self.serializer.code_verifier(code_verifier.as_str()); } - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ClientSecret), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::AuthorizationCode), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), - SerializerField::NotRequired(OAuthParameter::CodeVerifier), - ]); + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::AuthorizationCode, + OAuthParameter::GrantType, + ], + ); } - AuthorizationFailure::required_value_msg_result( - &format!( + AF::msg_result( + format!( "{} or {}", OAuthParameter::AuthorizationCode.alias(), OAuthParameter::RefreshToken.alias() ), - Some("Either authorization code or refresh token is required"), + "Either authorization code or refresh token is required", ) } @@ -221,7 +214,7 @@ impl AuthorizationCodeCredentialBuilder { credential: AuthorizationCodeCredential { authorization_code: None, refresh_token: None, - client_id: String::new(), + client_id: String::with_capacity(32), client_secret: String::new(), redirect_uri: Url::parse("http://localhost") .expect("Internal Error - please report"), @@ -257,7 +250,7 @@ impl AuthorizationCodeCredentialBuilder { self } - pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); self } @@ -337,4 +330,21 @@ mod test { let mut credential = credential_builder.build(); let _ = credential.form_urlencode().unwrap(); } + + #[test] + fn serialization() { + let mut credential_builder = AuthorizationCodeCredential::builder(); + let mut credential = credential_builder + .with_redirect_uri("https://localhost") + .unwrap() + .with_client_id("client_id") + .with_client_secret("client_secret") + .with_scope(vec!["scope"]) + .with_tenant("tenant_id") + .with_authorization_code("authorization_code") + .build(); + + let map = credential.form_urlencode().unwrap(); + assert_eq!(map.get("client_id"), Some(&String::from("client_id"))) + } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 5d7852d9..c67a2497 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, TokenCredentialOptions, TokenRequest, @@ -82,18 +81,12 @@ impl AuthorizationSerializer for ClientCertificateCredential { if self.refresh_token.is_none() { let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "access_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "refresh_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } @@ -101,13 +94,11 @@ impl AuthorizationSerializer for ClientCertificateCredential { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::required_value_result( - OAuthParameter::ClientAssertion.alias(), - ); + return AuthorizationFailure::result(OAuthParameter::ClientAssertion.alias()); } if self.client_assertion_type.trim().is_empty() { @@ -126,9 +117,9 @@ impl AuthorizationSerializer for ClientCertificateCredential { return if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - Some("refresh_token is set but is empty"), + "refresh_token is set but is empty", ); } @@ -136,23 +127,28 @@ impl AuthorizationSerializer for ClientCertificateCredential { .refresh_token(refresh_token.as_ref()) .grant_type("refresh_token"); - self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::RefreshToken), - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::Required(OAuthParameter::ClientAssertion), - SerializerField::Required(OAuthParameter::ClientAssertionType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]) + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + OAuthParameter::RefreshToken, + ], + ) } else { self.serializer.grant_type("client_credentials"); - self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::Required(OAuthParameter::ClientAssertion), - SerializerField::Required(OAuthParameter::ClientAssertionType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]) + + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ) }; } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index f3ff5d2b..f69334b8 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -37,13 +37,11 @@ impl ClientCredentialsAuthorizationUrl { ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_result( - OAuthParameter::RedirectUri.alias(), - ); + return AuthorizationFailure::result(OAuthParameter::RedirectUri.alias()); } serializer @@ -69,12 +67,12 @@ impl ClientCredentialsAuthorizationUrl { let mut url = Url::parse( serializer .get(OAuthParameter::AuthorizationUrl) - .ok_or(AuthorizationFailure::required_value( + .ok_or(AuthorizationFailure::err( OAuthParameter::AuthorizationUrl.alias(), ))? .as_str(), ) - .or(AuthorizationFailure::required_value_result( + .or(AuthorizationFailure::result( OAuthParameter::AuthorizationUrl.alias(), ))?; url.set_query(Some(encoder.finish().as_str())); diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index aca831d4..641478fa 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, ClientCredentialsAuthorizationUrlBuilder, CredentialBuilder, TokenRequest, @@ -95,18 +94,18 @@ impl AuthorizationSerializer for ClientSecretCredential { .authority(azure_authority_host, &self.authority); let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId); + return AuthorizationFailure::result(OAuthParameter::ClientId); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientSecret); + return AuthorizationFailure::result(OAuthParameter::ClientSecret); } self.serializer.grant_type("client_credentials"); @@ -118,10 +117,8 @@ impl AuthorizationSerializer for ClientSecretCredential { self.serializer.extend_scopes(&self.scope); } - self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]) + self.serializer + .as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) } /// diff --git a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs index f47aa2aa..b2f3cb40 100644 --- a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::form_credential::SerializerField; use crate::oauth::ResponseType; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -50,15 +49,15 @@ impl CodeFlowAuthorizationUrl { pub fn url(&self) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("redirect_uri", None); + return AuthorizationFailure::result("redirect_uri"); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("client_id", None); + return AuthorizationFailure::result("client_id"); } if self.scope.is_empty() { - return AuthorizationFailure::required_value_msg_result("scope", None); + return AuthorizationFailure::result("scope"); } serializer @@ -69,12 +68,14 @@ impl CodeFlowAuthorizationUrl { .response_type(self.response_type.clone()); let mut encoder = Serializer::new(String::new()); - serializer.url_query_encode( + + serializer.encode_query( + vec![], vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::Required(OAuthParameter::ResponseType), + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::Scope, + OAuthParameter::ResponseType, ], &mut encoder, )?; @@ -84,10 +85,7 @@ impl CodeFlowAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg_result( - "authorization_url", - Some("Internal Error"), - ) + AuthorizationFailure::msg_result("authorization_url", "Internal Error") } } } diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs index c7f49afa..c004a555 100644 --- a/graph-oauth/src/identity/credentials/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -62,9 +61,9 @@ impl CodeFlowCredential { impl AuthorizationSerializer for CodeFlowCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { if azure_authority_host.ne(&AzureAuthorityHost::OneDriveAndSharePoint) { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( "uri", - Some("Code flow can only be used with AzureAuthorityHost::OneDriveAndSharePoint"), + "Code flow can only be used with AzureAuthorityHost::OneDriveAndSharePoint", ); } @@ -73,47 +72,28 @@ impl AuthorizationSerializer for CodeFlowCredential { if self.refresh_token.is_none() { let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "access_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "refresh_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.authorization_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg_result( - &format!( - "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - Some("Authorization code and refresh token should not be set at the same time - Internal Error"), - ); - } - if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::required_value_result( - OAuthParameter::ClientSecret.alias(), - ); + return AuthorizationFailure::result(OAuthParameter::ClientSecret.alias()); } if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::RedirectUri); + return AuthorizationFailure::result(OAuthParameter::RedirectUri); } self.serializer @@ -124,46 +104,52 @@ impl AuthorizationSerializer for CodeFlowCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - Some("Either authorization code or refresh token is required"), + "Either authorization code or refresh token is required", ); } self.serializer.refresh_token(refresh_token.as_ref()); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ClientSecret), - SerializerField::Required(OAuthParameter::RefreshToken), - SerializerField::Required(OAuthParameter::RedirectUri), - ]); + return self.serializer.as_credential_map( + vec![], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::RefreshToken, + ], + ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - Some("Either authorization code or refresh token is required"), + "Either authorization code or refresh token is required", ); } self.serializer .authorization_code(authorization_code.as_ref()); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ClientSecret), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::AuthorizationCode), - ]); + return self.serializer.as_credential_map( + vec![], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::AuthorizationCode, + ], + ); } - AuthorizationFailure::required_value_msg_result( - &format!( + AuthorizationFailure::msg_result( + format!( "{} or {}", OAuthParameter::AuthorizationCode.alias(), OAuthParameter::RefreshToken.alias() ), - Some("Either authorization code or refresh token is required"), + "Either authorization code or refresh token is required", ) } } diff --git a/graph-oauth/src/identity/credentials/crypto.rs b/graph-oauth/src/identity/credentials/crypto.rs index 590cf432..29143fd0 100644 --- a/graph-oauth/src/identity/credentials/crypto.rs +++ b/graph-oauth/src/identity/credentials/crypto.rs @@ -5,19 +5,23 @@ use ring::rand::SecureRandom; pub struct Crypto; impl Crypto { - pub fn secure_random_string() -> anyhow::Result<String> { + pub fn sha256_secure_string() -> anyhow::Result<(String, String)> { let mut buf = [0; 32]; let rng = ring::rand::SystemRandom::new(); rng.fill(&mut buf) .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + // Known as code_verifier in proof key for code exchange let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); let mut context = ring::digest::Context::new(&ring::digest::SHA256); context.update(base_64_random_string.as_bytes()); - let secure_random_string = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - Ok(secure_random_string) + // Known as code_challenge in proof key for code exchange + let secure_string = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + + // code verifier, code challenge + Ok((base_64_random_string, secure_string)) } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 985776f3..ff8a1805 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -2,7 +2,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; -use crate::oauth::form_credential::SerializerField; use crate::oauth::DeviceCode; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; @@ -71,18 +70,12 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { if self.refresh_token.is_none() { let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "access_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::required_value_msg( - "refresh_token_url", - Some("Internal Error"), - ), + AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } @@ -90,18 +83,18 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.device_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::required_value_msg_result( - &format!( + return AuthorizationFailure::msg_result( + format!( "{} or {}", OAuthParameter::DeviceCode.alias(), OAuthParameter::RefreshToken.alias() ), - Some("Device code and refresh token should not be set at the same time - Internal Error"), + "Device code and refresh token should not be set at the same time - Internal Error", ); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } self.serializer @@ -110,9 +103,9 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - Some("Either device code or refresh token is required - found empty refresh token"), + "Either device code or refresh token is required - found empty refresh token", ); } @@ -120,19 +113,20 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { .grant_type("refresh_token") .device_code(refresh_token.as_ref()); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::RefreshToken), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::Required(OAuthParameter::GrantType), - ]); + return self.serializer.as_credential_map( + vec![], + vec![ + OAuthParameter::ClientId, + OAuthParameter::RefreshToken, + OAuthParameter::Scope, + OAuthParameter::GrantType, + ], + ); } else if let Some(device_code) = self.device_code.as_ref() { if device_code.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( OAuthParameter::DeviceCode.alias(), - Some( - "Either device code or refresh token is required - found empty device code", - ), + "Either device code or refresh token is required - found empty device code", ); } @@ -140,21 +134,24 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { .grant_type(DEVICE_CODE_GRANT_TYPE) .device_code(device_code.as_ref()); - return self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::DeviceCode), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::Required(OAuthParameter::GrantType), - ]); + return self.serializer.as_credential_map( + vec![], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::Scope, + OAuthParameter::GrantType, + ], + ); } - AuthorizationFailure::required_value_msg_result( - &format!( + AuthorizationFailure::msg_result( + format!( "{} or {}", OAuthParameter::DeviceCode.alias(), OAuthParameter::RefreshToken.alias() ), - Some("Either device code or refresh token is required"), + "Either device code or refresh token is required", ) } } diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index be01d16e..bbe4260a 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AzureAuthorityHost, CredentialBuilder, Crypto, Prompt, ResponseMode, ResponseType, }; @@ -131,11 +130,11 @@ impl ImplicitCredential { let mut serializer = OAuthSerializer::new(); if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result("client_id"); + return AuthorizationFailure::result("client_id"); } if self.nonce.trim().is_empty() { - return AuthorizationFailure::required_value_result("nonce"); + return AuthorizationFailure::result("nonce"); } serializer @@ -179,13 +178,12 @@ impl ImplicitCredential { if self.response_type.contains(&ResponseType::IdToken) { serializer.add_scope("openid"); } else { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( "scope", - Some( - &format!("{} {}", - "scope must be provided or response_type must be id_token which will add openid to scope:", - "https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc" - ) + format!("{} {}", + "scope must be provided or response_type must be id_token which will add openid to scope:", + "https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc" + ) ); } @@ -207,31 +205,31 @@ impl ImplicitCredential { serializer.login_hint(login_hint.as_str()); } - let authorization_credentials = vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::ResponseType), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::Required(OAuthParameter::Nonce), - SerializerField::NotRequired(OAuthParameter::RedirectUri), - SerializerField::NotRequired(OAuthParameter::ResponseMode), - SerializerField::NotRequired(OAuthParameter::State), - SerializerField::NotRequired(OAuthParameter::Prompt), - SerializerField::NotRequired(OAuthParameter::LoginHint), - SerializerField::NotRequired(OAuthParameter::DomainHint), - ]; - let mut encoder = Serializer::new(String::new()); - serializer.url_query_encode(authorization_credentials, &mut encoder)?; + serializer.encode_query( + vec![ + OAuthParameter::RedirectUri, + OAuthParameter::ResponseMode, + OAuthParameter::State, + OAuthParameter::Prompt, + OAuthParameter::LoginHint, + OAuthParameter::DomainHint, + ], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ResponseType, + OAuthParameter::Scope, + OAuthParameter::Nonce, + ], + &mut encoder, + )?; if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg_result( - "authorization_url", - Some("Internal Error"), - ) + AuthorizationFailure::msg_result("authorization_url", "Internal Error") } } } @@ -319,7 +317,7 @@ impl ImplicitCredentialBuilder { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.credential.nonce = Crypto::secure_random_string()?; + self.credential.nonce = Crypto::sha256_secure_string()?.1; Ok(self) } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index c837d448..612f6014 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,6 +1,7 @@ #[macro_use] mod credential_builder; +mod as_query; mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; @@ -31,6 +32,7 @@ mod token_request; #[cfg(feature = "openssl")] mod x509_certificate; +pub use as_query::*; pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 96d98623..ae2c4b73 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -1,7 +1,11 @@ +use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType, + AsQuery, Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, + ResponseType, }; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use std::collections::BTreeSet; +use url::form_urlencoded::Serializer; use url::Url; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional @@ -19,10 +23,10 @@ pub struct OpenIdAuthorizationUrl { /// by your app. It must exactly match one of the redirect URIs you registered in the portal, /// except that it must be URL-encoded. If not present, the endpoint will pick one registered /// redirect_uri at random to send the user back to. - pub(crate) redirect_uri: String, + pub(crate) redirect_uri: Option<String>, /// Required /// Must include code for OpenID Connect sign-in. - pub(crate) response_type: Vec<ResponseType>, + pub(crate) response_type: BTreeSet<ResponseType>, /// Optional /// Specifies how the identity platform should return the requested token to your app. /// @@ -75,7 +79,7 @@ pub struct OpenIdAuthorizationUrl { /// Finally, [Prompt::SelectAccount] shows the user an account selector, negating silent SSO but /// allowing the user to pick which account they intend to sign in with, without requiring /// credential entry. You can't use both login_hint and select_account. - pub(crate) prompt: Vec<Prompt>, + pub(crate) prompt: BTreeSet<Prompt>, /// Optional /// The realm of the user in a federated directory. This skips the email-based discovery /// process that the user goes through on the sign-in page, for a slightly more streamlined @@ -92,19 +96,19 @@ pub struct OpenIdAuthorizationUrl { } impl OpenIdAuthorizationUrl { - pub fn new<T: AsRef<str>>( - client_id: T, - redirect_uri: T, - ) -> anyhow::Result<OpenIdAuthorizationUrl> { + pub fn new<T: AsRef<str>>(client_id: T) -> anyhow::Result<OpenIdAuthorizationUrl> { + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::Code); + Ok(OpenIdAuthorizationUrl { client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), - response_type: vec![ResponseType::Code], + redirect_uri: None, + response_type, response_mode: None, - nonce: Crypto::secure_random_string()?, + nonce: Crypto::sha256_secure_string()?.1, state: None, scope: vec!["openid".to_owned()], - prompt: vec![], + prompt: BTreeSet::new(), domain_hint: None, login_hint: None, authority: Authority::default(), @@ -125,11 +129,27 @@ impl OpenIdAuthorizationUrl { ) -> AuthorizationResult<Url> { self.authorization_url_with_host(azure_authority_host) } + + /// Get the nonce. + /// + /// This value may be generated automatically by the client and may be useful for users + /// who want to manually verify that the nonce stored in the client is the same as the + /// nonce returned in the response from the authorization server. + /// Verifying the nonce helps mitigate token replay attacks. + pub fn nonce(&mut self) -> &String { + &self.nonce + } } impl AuthorizationUrl for OpenIdAuthorizationUrl { fn redirect_uri(&self) -> AuthorizationResult<Url> { - Url::parse(self.redirect_uri.as_str()).map_err(AuthorizationFailure::from) + let redirect_uri = self.redirect_uri.as_ref() + .ok_or(AuthorizationFailure::msg_err( + "redirect_uri", + "If not provided, the authorization server will pick one registered redirect_uri at random to send the user back to" + ))?; + + Url::parse(redirect_uri.as_str()).map_err(AuthorizationFailure::from) } fn authorization_url(&self) -> AuthorizationResult<Url> { @@ -138,9 +158,76 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { fn authorization_url_with_host( &self, - _azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureAuthorityHost, ) -> AuthorizationResult<Url> { - unimplemented!() + let mut serializer = OAuthSerializer::new(); + + if self.client_id.trim().is_empty() { + return AuthorizationFailure::result("client_id"); + } + + if self.scope.is_empty() { + return AuthorizationFailure::result("scope"); + } + + if self.nonce.is_empty() { + return AuthorizationFailure::msg_result( + "nonce", + "nonce is empty - nonce can be automatically generated if not updated by the caller" + ); + } + + serializer + .client_id(self.client_id.as_str()) + .extend_scopes(self.scope.clone()) + .nonce(self.nonce.as_str()) + .authority(azure_authority_host, &self.authority); + + if let Some(redirect_uri) = self.redirect_uri.as_ref() { + serializer.redirect_uri(redirect_uri.as_ref()); + } + + if let Some(state) = self.state.as_ref() { + serializer.state(state.as_str()); + } + + if !self.prompt.is_empty() { + serializer.prompt(&self.prompt.as_query()); + } + + if let Some(domain_hint) = self.domain_hint.as_ref() { + serializer.domain_hint(domain_hint.as_str()); + } + + if let Some(login_hint) = self.login_hint.as_ref() { + serializer.login_hint(login_hint.as_str()); + } + + let mut encoder = Serializer::new(String::new()); + serializer.encode_query( + vec![ + OAuthParameter::RedirectUri, + OAuthParameter::ResponseMode, + OAuthParameter::State, + OAuthParameter::Prompt, + OAuthParameter::LoginHint, + OAuthParameter::DomainHint, + ], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ResponseType, + OAuthParameter::Scope, + OAuthParameter::Nonce, + ], + &mut encoder, + )?; + + let authorization_url = serializer + .get(OAuthParameter::AuthorizationUrl) + .ok_or(AF::msg_err("authorization_url", "Internal Error"))?; + let mut url = Url::parse(authorization_url.as_str())?; + url.set_query(Some(encoder.finish().as_str())); + Ok(url) } } @@ -149,14 +236,14 @@ pub struct OpenIdAuthorizationUrlBuilder { } impl OpenIdAuthorizationUrlBuilder { - fn new() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + pub(crate) fn new() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { Ok(OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl::new(String::new(), String::new())?, + auth_url_parameters: OpenIdAuthorizationUrl::new(String::with_capacity(32))?, }) } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.auth_url_parameters.redirect_uri = redirect_uri.as_ref().to_owned(); + self.auth_url_parameters.redirect_uri = Some(redirect_uri.as_ref().to_owned()); self } @@ -182,7 +269,9 @@ impl OpenIdAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.auth_url_parameters.response_type = response_type.into_iter().collect(); + self.auth_url_parameters + .response_type + .extend(response_type.into_iter()); self } @@ -229,8 +318,8 @@ impl OpenIdAuthorizationUrlBuilder { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] - fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.nonce = Crypto::secure_random_string()?; + pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { + self.auth_url_parameters.nonce = Crypto::sha256_secure_string()?.1; Ok(self) } @@ -265,7 +354,7 @@ impl OpenIdAuthorizationUrlBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self { - self.auth_url_parameters.prompt = prompt.into_iter().collect(); + self.auth_url_parameters.prompt.extend(prompt.into_iter()); self } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index ae859e87..78419666 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1 +1,304 @@ -pub struct OpenIdCredential {} +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::{ + Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, + OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, +}; +use crate::oauth::OpenIdAuthorizationUrlBuilder; +use async_trait::async_trait; +use graph_error::{AuthorizationResult, AF}; +use reqwest::IntoUrl; +use std::collections::HashMap; +use url::Url; + +credential_builder_impl!(OpenIdCredentialBuilder, OpenIdCredential); + +/// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional +/// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your +/// OAuth-enabled applications by using a security token called an ID token. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc +#[derive(Clone)] +pub struct OpenIdCredential { + /// Required unless requesting a refresh token + /// The authorization code obtained from a call to authorize. + /// The code should be obtained with all required scopes. + pub(crate) authorization_code: Option<String>, + /// Required when requesting a new access token using a refresh token + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + /// Required + /// The application secret that you created in the app registration portal for your app. + /// Don't use the application secret in a native app or single page app because a + /// client_secret can't be reliably stored on devices or web pages. It's required for web + /// apps and web APIs, which can store the client_secret securely on the server side. Like + /// all parameters here, the client secret must be URL-encoded before being sent. This step + /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax + /// specification. The Basic auth pattern of instead providing credentials in the Authorization + /// header, per RFC 6749 is also supported. + pub(crate) client_secret: String, + /// The same redirect_uri value that was used to acquire the authorization_code. + pub(crate) redirect_uri: Url, + /// A space-separated list of scopes. The scopes must all be from a single resource, + /// along with OIDC scopes (profile, openid, email). For more information, see Permissions + /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension + /// to the authorization code flow, intended to allow apps to declare the resource they want + /// the token for during token redemption. + pub(crate) scope: Vec<String>, + /// The Azure Active Directory tenant (directory) Id of the service principal. + pub(crate) authority: Authority, + /// The same code_verifier that was used to obtain the authorization_code. + /// Required if PKCE was used in the authorization code grant request. For more information, + /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. + pub(crate) code_verifier: Option<String>, + pub(crate) token_credential_options: TokenCredentialOptions, + serializer: OAuthSerializer, +} + +impl OpenIdCredential { + pub fn new<T: AsRef<str>, U: IntoUrl>( + client_id: T, + client_secret: T, + authorization_code: T, + redirect_uri: U, + ) -> AuthorizationResult<OpenIdCredential> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + + Ok(OpenIdCredential { + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_id: client_id.as_ref().to_owned(), + client_secret: client_secret.as_ref().to_owned(), + redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, + scope: vec!["openid".to_owned()], + authority: Default::default(), + code_verifier: None, + token_credential_options: TokenCredentialOptions::default(), + serializer: OAuthSerializer::new(), + }) + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { + self.authorization_code = None; + self.refresh_token = Some(refresh_token.as_ref().to_owned()); + } + + pub fn builder() -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new() + } + + pub fn authorization_url_builder() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + OpenIdAuthorizationUrlBuilder::new() + } +} + +#[async_trait] +impl TokenRequest for OpenIdCredential { + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } +} + +impl AuthorizationSerializer for OpenIdCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority); + + if self.refresh_token.is_none() { + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) + } else { + let uri = self + .serializer + .get(OAuthParameter::RefreshTokenUrl) + .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) + } + } + + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + if self.client_id.trim().is_empty() { + return AF::result(OAuthParameter::ClientId.alias()); + } + + if self.client_secret.trim().is_empty() { + return AF::result(OAuthParameter::ClientSecret.alias()); + } + + self.serializer + .client_id(self.client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .extend_scopes(self.scope.clone()); + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); + } + + self.serializer + .grant_type("refresh_token") + .refresh_token(refresh_token.as_ref()); + + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RefreshToken, + OAuthParameter::GrantType, + ], + ); + } else if let Some(authorization_code) = self.authorization_code.as_ref() { + if authorization_code.trim().is_empty() { + return AF::msg_result( + OAuthParameter::AuthorizationCode.alias(), + "Authorization code is empty", + ); + } + + self.serializer + .authorization_code(authorization_code.as_ref()) + .grant_type("authorization_code") + .redirect_uri(self.redirect_uri.as_str()); + + if let Some(code_verifier) = self.code_verifier.as_ref() { + self.serializer.code_verifier(code_verifier.as_str()); + } + + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RedirectUri, + OAuthParameter::AuthorizationCode, + OAuthParameter::GrantType, + ], + ); + } + + AF::msg_result( + format!( + "{} or {}", + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() + ), + "Either authorization code or refresh token is required", + ) + } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } +} + +#[derive(Clone)] +pub struct OpenIdCredentialBuilder { + credential: OpenIdCredential, +} + +impl OpenIdCredentialBuilder { + fn new() -> OpenIdCredentialBuilder { + Self { + credential: OpenIdCredential { + authorization_code: None, + refresh_token: None, + client_id: String::with_capacity(32), + client_secret: String::new(), + redirect_uri: Url::parse("http://localhost") + .expect("Internal Error - please report"), + scope: vec![], + authority: Default::default(), + code_verifier: None, + token_credential_options: TokenCredentialOptions::default(), + serializer: OAuthSerializer::new(), + }, + } + } + + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); + self.credential.refresh_token = None; + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.authorization_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + /// Defaults to http://localhost + pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { + self.credential.redirect_uri = redirect_uri.into_url()?; + Ok(self) + } + + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.credential.client_secret = client_secret.as_ref().to_owned(); + self + } + + fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + self + } + + pub fn with_proof_key_for_code_exchange( + &mut self, + proof_key_for_code_exchange: &ProofKeyForCodeExchange, + ) -> &mut Self { + self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); + self + } +} + +impl From<OpenIdAuthorizationUrl> for OpenIdCredentialBuilder { + fn from(value: OpenIdAuthorizationUrl) -> Self { + let mut builder = OpenIdCredentialBuilder::new(); + if let Some(redirect_uri) = value.redirect_uri.as_ref() { + let _ = builder.with_redirect_uri(redirect_uri); + } + builder + .with_scope(value.scope) + .with_client_secret(value.client_id) + .with_authority(value.authority); + + builder + } +} + +impl From<OpenIdCredential> for OpenIdCredentialBuilder { + fn from(credential: OpenIdCredential) -> Self { + OpenIdCredentialBuilder { credential } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn with_tenant_id_common() { + let credential = OpenIdCredential::builder() + .with_authority(Authority::TenantId("common".into())) + .build(); + + assert_eq!(credential.authority, Authority::TenantId("common".into())) + } + + #[test] + fn with_tenant_id_adfs() { + let credential = OpenIdCredential::builder() + .with_authority(Authority::AzureDirectoryFederatedServices) + .build(); + + assert_eq!(credential.authority.as_ref(), "adfs"); + } +} diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index bfa70587..67ef34a0 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -1,3 +1,6 @@ +use crate::identity::credentials::as_query::AsQuery; +use std::collections::BTreeSet; + /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// @@ -47,3 +50,21 @@ impl IntoIterator for Prompt { vec![self].into_iter() } } + +impl AsQuery for Vec<Prompt> { + fn as_query(&self) -> String { + self.iter() + .map(|s| s.as_ref()) + .collect::<Vec<&str>>() + .join(" ") + } +} + +impl AsQuery for BTreeSet<Prompt> { + fn as_query(&self) -> String { + self.iter() + .map(|s| s.as_ref()) + .collect::<Vec<&str>>() + .join(" ") + } +} diff --git a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs index 3a000ccd..ec696f67 100644 --- a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs +++ b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs @@ -1,5 +1,4 @@ -use base64::Engine; -use ring::rand::SecureRandom; +use crate::oauth::Crypto; #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ProofKeyForCodeExchange { @@ -51,16 +50,7 @@ impl ProofKeyForCodeExchange { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. pub fn generate() -> anyhow::Result<ProofKeyForCodeExchange> { - let mut buf = [0; 32]; - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; - let code_verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf); - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(code_verifier.as_bytes()); - let code_challenge = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - + let (code_verifier, code_challenge) = Crypto::sha256_secure_string()?; Ok(ProofKeyForCodeExchange { code_verifier, code_challenge, diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index e8eb8fff..6f61eded 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::form_credential::SerializerField; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; @@ -75,22 +74,22 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { .authority(azure_authority_host, &self.authority); let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::required_value_msg("access_token_url", Some("Internal Error")), + AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } if self.username.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::Username.alias()); + return AuthorizationFailure::result(OAuthParameter::Username.alias()); } if self.password.trim().is_empty() { - return AuthorizationFailure::required_value_result(OAuthParameter::Password.alias()); + return AuthorizationFailure::result(OAuthParameter::Password.alias()); } self.serializer @@ -98,11 +97,10 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { .grant_type("password") .extend_scopes(self.scope.iter()); - self.serializer.authorization_form(vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::GrantType), - SerializerField::NotRequired(OAuthParameter::Scope), - ]) + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![OAuthParameter::ClientId, OAuthParameter::GrantType], + ) } fn basic_auth(&self) -> Option<(String, String)> { @@ -165,9 +163,9 @@ impl ResourceOwnerPasswordCredentialBuilder { || authority.eq(&Authority::AzureActiveDirectory) || authority.eq(&Authority::Consumers) { - return AuthorizationFailure::required_value_msg_result( + return AuthorizationFailure::msg_result( "tenant_id", - Some("The grant type isn't supported on the /common or /consumers authentication contexts") + "Authority Azure Active Directory, common, and consumers are not supported authentication contexts for ROPC" ); } diff --git a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs index 21f445cf..ac090d6d 100644 --- a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs @@ -1,5 +1,4 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::form_credential::SerializerField; use crate::oauth::ResponseType; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -37,15 +36,15 @@ impl TokenFlowAuthorizationUrl { pub fn url(&self) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("redirect_uri", None); + return AuthorizationFailure::result("redirect_uri"); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::required_value_msg_result("client_id", None); + return AuthorizationFailure::result("client_id"); } if self.scope.is_empty() { - return AuthorizationFailure::required_value_msg_result("scope", None); + return AuthorizationFailure::result("scope"); } serializer @@ -56,12 +55,13 @@ impl TokenFlowAuthorizationUrl { .response_type(self.response_type.clone()); let mut encoder = Serializer::new(String::new()); - serializer.url_query_encode( + serializer.encode_query( + vec![], vec![ - SerializerField::Required(OAuthParameter::ClientId), - SerializerField::Required(OAuthParameter::RedirectUri), - SerializerField::Required(OAuthParameter::Scope), - SerializerField::Required(OAuthParameter::ResponseType), + OAuthParameter::ClientId, + OAuthParameter::RedirectUri, + OAuthParameter::Scope, + OAuthParameter::ResponseType, ], &mut encoder, )?; @@ -71,10 +71,7 @@ impl TokenFlowAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::required_value_msg_result( - "authorization_url", - Some("Internal Error"), - ) + AuthorizationFailure::msg_result("authorization_url", "Internal Error") } } } diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 4dcefcda..a4cd74a0 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -1,4 +1,4 @@ -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use openssl::error::ErrorStack; diff --git a/graph-oauth/src/identity/form_credential.rs b/graph-oauth/src/identity/form_credential.rs index 184a3edb..d351b1cc 100644 --- a/graph-oauth/src/identity/form_credential.rs +++ b/graph-oauth/src/identity/form_credential.rs @@ -1,7 +1,7 @@ use crate::auth::OAuthParameter; -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum SerializerField { +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum ParameterIs { Required(OAuthParameter), - NotRequired(OAuthParameter), + Optional(OAuthParameter), } diff --git a/test-tools/.cargo/config.toml b/test-tools/.cargo/config.toml new file mode 100644 index 00000000..a41675fd --- /dev/null +++ b/test-tools/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +GRAPH_RS_SDK = "1.1.1" From 369a810cc371b38e057bee4c5fd02ecba6861020 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 27 May 2023 21:42:29 -0400 Subject: [PATCH 020/118] Add initial token cache impl and update test-tools to use identity credentials --- examples/oauth/auth_code_grant_pkce.rs | 2 +- examples/oauth/enable_pii_logging.rs | 12 + examples/oauth/main.rs | 9 +- graph-oauth/src/access_token.rs | 310 ++++++------------ graph-oauth/src/auth.rs | 4 +- graph-oauth/src/id_token.rs | 14 +- .../in_memory_credential_store.rs | 59 ++++ .../src/identity/credential_store/mod.rs | 44 +++ .../credential_store/token_cache_providers.rs | 7 + ...thorization_code_certificate_credential.rs | 8 +- .../authorization_code_credential.rs | 9 +- .../credentials/client_application.rs | 64 ++++ .../client_certificate_credential.rs | 8 +- .../credentials/client_secret_credential.rs | 10 +- .../credentials/code_flow_credential.rs | 2 +- .../confidential_client_application.rs | 62 +++- .../credentials/device_code_credential.rs | 22 +- .../credentials/environment_credential.rs | 19 +- graph-oauth/src/identity/credentials/mod.rs | 4 + .../credentials/open_id_credential.rs | 9 +- .../credentials/public_client_application.rs | 10 +- .../resource_owner_password_credential.rs | 23 +- .../identity/credentials/token_credential.rs | 7 +- .../credentials/token_credential_options.rs | 21 ++ graph-oauth/src/identity/mod.rs | 2 + src/client/graph.rs | 2 +- test-tools/src/oauth_request.rs | 140 ++++---- tests/access_token_request.rs | 2 +- tests/access_token_tests.rs | 30 -- tests/mail_folder_request.rs | 10 +- tests/onenote_request.rs | 15 +- 31 files changed, 575 insertions(+), 365 deletions(-) create mode 100644 examples/oauth/enable_pii_logging.rs create mode 100644 graph-oauth/src/identity/credential_store/in_memory_credential_store.rs create mode 100644 graph-oauth/src/identity/credential_store/mod.rs create mode 100644 graph-oauth/src/identity/credential_store/token_cache_providers.rs create mode 100644 graph-oauth/src/identity/credentials/client_application.rs create mode 100644 graph-oauth/src/identity/credentials/token_credential_options.rs diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 81ebf695..81a84b71 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -81,7 +81,7 @@ async fn handle_redirect( let access_token: AccessToken = response.json().await.unwrap(); // If all went well here we can print out the OAuth config with the Access Token. - println!("AccessToken: {:#?}", access_token.bearer_token()); + println!("AccessToken: {:#?}", access_token.access_token); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; diff --git a/examples/oauth/enable_pii_logging.rs b/examples/oauth/enable_pii_logging.rs new file mode 100644 index 00000000..ac3cd651 --- /dev/null +++ b/examples/oauth/enable_pii_logging.rs @@ -0,0 +1,12 @@ +// By default the AccessToken access_token (bearer token) and id_token fields +// are logged or printed to the console as [REDACTED] by the AccessToken Debug implementation. + +// You can enable logging of these fields by setting the enable personally +// identifiable information field to true called enable_pii. + +use graph_rs_sdk::oauth::AccessToken; + +fn enable_pii_on_access_token(access_token: &mut AccessToken) { + access_token.enable_pii_logging(true); + println!("{access_token:#?}"); +} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index c27a53b6..20aa5fdb 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -21,6 +21,7 @@ mod auth_code_grant_refresh_token; mod client_credentials; mod client_credentials_admin_consent; mod device_code; +mod enable_pii_logging; mod environment_credential; mod implicit_grant; mod is_access_token_expired; @@ -30,8 +31,8 @@ mod signing_keys; use graph_rs_sdk::oauth::{ AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - CredentialBuilder, DeviceAuthorizationCredential, ProofKeyForCodeExchange, - PublicClientApplication, TokenRequest, + CredentialBuilder, DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, + TokenRequest, }; #[tokio::main] @@ -74,7 +75,7 @@ async fn auth_code_grant(authorization_code: &str) { println!("{response:#?}"); let access_token: AccessToken = response.json().await.unwrap(); - println!("{:#?}", access_token.bearer_token()); + println!("{:#?}", access_token.access_token); } // Client Credentials Grant @@ -86,5 +87,5 @@ async fn client_credentials() { println!("{response:#?}"); let access_token: AccessToken = response.json().await.unwrap(); - println!("{:#?}", access_token.bearer_token()); + println!("{:#?}", access_token.access_token); } diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index be7a8629..8ccdd901 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -3,12 +3,33 @@ use crate::jwt::{Claim, JsonWebToken, JwtParser}; use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc}; use chrono_humanize::HumanTime; use graph_error::GraphFailure; +use serde::{Deserialize, Deserializer}; use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; +use std::fmt::format; use std::str::FromStr; +// Used to set timestamp based on expires in +// which can only be done after deserialization. +#[derive(Clone, Serialize, Deserialize)] +struct PhantomAccessToken { + access_token: String, + token_type: String, + #[serde(deserialize_with = "deserialize_number_from_string")] + expires_in: i64, + /// Legacy version of expires_in + ext_expires_in: Option<i64>, + scope: Option<String>, + refresh_token: Option<String>, + user_id: Option<String>, + id_token: Option<String>, + state: Option<String>, + #[serde(flatten)] + additional_fields: HashMap<String, Value>, +} + /// OAuth 2.0 Access Token /// /// Create a new AccessToken. @@ -17,65 +38,51 @@ use std::str::FromStr; /// # use graph_oauth::oauth::AccessToken; /// let access_token = AccessToken::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); /// ``` -/// -/// You can also get the claims using the claims() method as well as -/// the remaining duration that the access token is valid using the elapsed() -/// method. -/// +/// The [AccessToken::jwt] method attempts to parse the access token as a JWT. /// Tokens returned for personal microsoft accounts that use legacy MSA /// are encrypted and cannot be parsed. This bearer token may still be /// valid but the jwt() method will return None. /// For more info see: -/// [Microsoft identity platform acccess tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) +/// [Microsoft identity platform access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// /// * Access Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.4 /// * Refresh Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.5 /// -/// For tokens where the JWT can be parsed the elapsed() method uses -/// the `exp` field in the JWT's claims. If the claims do not contain an -/// `exp` field or the token could not be parsed the elapsed() method -/// uses the expires_in field returned in the response body to caculate -/// the remaining time. These fields are only used once during -/// initialization to set a timestamp for future expiration of the access -/// token. -/// /// # Example /// ``` /// # use graph_oauth::oauth::AccessToken; /// # let mut access_token = AccessToken::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); /// -/// // Claims -/// println!("{:#?}", access_token.claims()); -/// /// // Duration left until expired. /// println!("{:#?}", access_token.elapsed()); /// ``` -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Eq, PartialEq, Serialize)] pub struct AccessToken { - access_token: String, - token_type: String, + pub access_token: String, + pub token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] - expires_in: i64, - scope: Option<String>, - refresh_token: Option<String>, - user_id: Option<String>, - id_token: Option<String>, - state: Option<String>, - timestamp: Option<DateTime<Utc>>, - #[serde(skip)] - jwt: Option<JsonWebToken>, + pub expires_in: i64, + /// Legacy version of expires_in + pub ext_expires_in: Option<i64>, + pub scope: Option<String>, + pub refresh_token: Option<String>, + pub user_id: Option<String>, + pub id_token: Option<String>, + pub state: Option<String>, + pub timestamp: Option<DateTime<Utc>>, /// Any extra returned fields for AccessToken. #[serde(flatten)] - additional_fields: HashMap<String, Value>, + pub additional_fields: HashMap<String, Value>, #[serde(skip)] log_pii: bool, } impl AccessToken { pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> AccessToken { - let mut token = AccessToken { + AccessToken { token_type: token_type.into(), - expires_in, + ext_expires_in: Some(expires_in.clone()), + expires_in: expires_in.clone(), scope: Some(scope.into()), access_token: access_token.into(), refresh_token: None, @@ -83,12 +90,9 @@ impl AccessToken { id_token: None, state: None, timestamp: Some(Utc::now() + Duration::seconds(expires_in)), - jwt: None, additional_fields: Default::default(), log_pii: false, - }; - token.parse_jwt(); - token + } } /// Set the token type. @@ -115,7 +119,7 @@ impl AccessToken { /// access_token.set_expires_in(3600); /// ``` pub fn set_expires_in(&mut self, expires_in: i64) -> &mut AccessToken { - self.expires_in = expires_in; + self.expires_in = expires_in.clone(); self.timestamp = Some(Utc::now() + Duration::seconds(expires_in)); self } @@ -231,143 +235,43 @@ impl AccessToken { self.log_pii = log_pii; } - /// Reset the access token timestmap. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// access_token.timestamp(); - /// // The timestamp is in UTC. - /// ``` - pub fn gen_timestamp(&mut self) { - self.timestamp = Some(Utc::now() + Duration::seconds(self.expires_in)); - } - - /// Get the token type. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.token_type()); - /// ``` - pub fn token_type(&self) -> &str { - self.token_type.as_str() - } - - /// Set the user id. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// // This is the original amount that was set not the difference. - /// // To get the difference you can use access_token.elapsed(). - /// println!("{:#?}", access_token.expires_in()); - /// ``` - pub fn expires_in(&self) -> i64 { - self.expires_in - } - - /// Get the scopes. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.scopes()); - /// ``` - pub fn scopes(&self) -> Option<&String> { - self.scope.as_ref() - } - - /// Get the access token. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.bearer_token()); - /// ``` - pub fn bearer_token(&self) -> &str { - self.access_token.as_str() - } - - /// Get the user id. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; - /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.user_id()); - /// ``` - pub fn user_id(&self) -> Option<String> { - self.user_id.clone() - } - - /// Get the refresh token. + /// Timestamp field is used to tell whether the access token is expired. + /// This method is mainly used internally as soon as the access token + /// is deserialized from the api response for an accurate reading + /// on when the access token expires. /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// You most likely do not want to use this method unless you are deserializing + /// the access token using custom deserialization or creating your own access tokens + /// manually. /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.refresh_token()); - /// ``` - pub fn refresh_token(&self) -> Option<String> { - self.refresh_token.clone() - } - - /// Get the id token. + /// This method resets the access token timestamp based on the expires_in field + /// which is the total seconds that the access token is valid for starting + /// from when the token was first retrieved. /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// This will reset the the timestamp from Utc Now + expires_in. This means + /// that if calling [AccessToken::gen_timestamp] will only be reliable if done + /// when the access token is first retrieved. /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.id_token()); - /// ``` - pub fn id_token(&self) -> Option<String> { - self.id_token.clone() - } - - /// Get the state. /// /// # Example /// ``` /// # use graph_oauth::oauth::AccessToken; /// /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.state()); + /// access_token.expires_in = 86999; + /// access_token.gen_timestamp(); + /// println!("{:#?}", access_token.timestamp); + /// // The timestamp is in UTC. /// ``` - pub fn state(&self) -> Option<String> { - self.state.clone() + pub fn gen_timestamp(&mut self) { + self.timestamp = Some(Utc::now() + Duration::seconds(self.expires_in.clone())); } - /// Get the timestamp. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// Check whether the access token is expired. Uses the expires_in + /// field to check time elapsed since token was first deserialized. + /// This is done using a Utc timestamp set when the [AccessToken] is + /// deserialized from the api response /// - /// let mut access_token = AccessToken::default(); - /// println!("{:#?}", access_token.timestamp()); - /// ``` - pub fn timestamp(&self) -> Option<DateTime<Utc>> { - self.timestamp - } - - // TODO: This should checked using the bearer token. - /// Check whether the access token is expired. An access token is considerd - /// expired when there is a negative difference between the timestamp set - /// for the access token and the expires_in field. /// /// # Example /// ``` @@ -383,7 +287,6 @@ impl AccessToken { true } - // TODO: This should checked using the bearer token. /// Get the time left in seconds until the access token expires. /// See the HumanTime crate. If you just need to know if the access token /// is expired then use the is_expired() message which returns a boolean @@ -404,47 +307,8 @@ impl AccessToken { None } - fn parse_jwt(&mut self) -> Option<&JsonWebToken> { - let mut set_timestamp = false; - if let Ok(jwt) = JwtParser::parse(self.bearer_token()) { - if let Some(claims) = jwt.claims() { - if let Some(claim) = claims - .iter() - .find(|item| item.key().eq(&String::from("exp"))) - { - let value = claim.value(); - let number = value.as_i64().unwrap(); - let local_result = Utc.timestamp_opt(number, 0); - if let LocalResult::Single(date_time) = local_result { - self.timestamp = Some(date_time); - set_timestamp = true; - } - } - } - self.jwt = Some(jwt); - } - - if !set_timestamp { - self.gen_timestamp(); - } - - self.jwt.as_ref() - } - - pub fn claims(&mut self) -> Option<Vec<Claim>> { - if self.jwt.is_none() { - self.parse_jwt(); - } - - self.jwt.as_ref()?.claims() - } - - pub fn jwt(&mut self) -> Option<&JsonWebToken> { - if self.jwt.is_none() { - return self.parse_jwt(); - } - - self.jwt.as_ref() + pub fn jwt(&mut self) -> Option<JsonWebToken> { + JwtParser::parse(self.access_token.as_str()).ok() } } @@ -453,6 +317,7 @@ impl Default for AccessToken { AccessToken { token_type: String::new(), expires_in: 0, + ext_expires_in: Some(0), scope: None, access_token: String::new(), refresh_token: None, @@ -460,7 +325,6 @@ impl Default for AccessToken { id_token: None, state: None, timestamp: Some(Utc::now() + Duration::seconds(0)), - jwt: None, additional_fields: Default::default(), log_pii: false, } @@ -471,9 +335,7 @@ impl TryFrom<&str> for AccessToken { type Error = GraphFailure; fn try_from(value: &str) -> Result<Self, Self::Error> { - let mut access_token: AccessToken = serde_json::from_str(value)?; - access_token.parse_jwt(); - Ok(access_token) + Ok(serde_json::from_str(value)?) } } @@ -482,8 +344,7 @@ impl TryFrom<reqwest::blocking::RequestBuilder> for AccessToken { fn try_from(value: reqwest::blocking::RequestBuilder) -> Result<Self, Self::Error> { let response = value.send()?; - let access_token: AccessToken = AccessToken::try_from(response)?; - Ok(access_token) + Ok(AccessToken::try_from(response)?) } } @@ -502,9 +363,7 @@ impl TryFrom<reqwest::blocking::Response> for AccessToken { type Error = GraphFailure; fn try_from(value: reqwest::blocking::Response) -> Result<Self, Self::Error> { - let mut access_token = value.json::<AccessToken>()?; - access_token.parse_jwt(); - Ok(access_token) + Ok(value.json::<AccessToken>()?) } } @@ -521,7 +380,7 @@ impl fmt::Debug for AccessToken { .field("id_token", &self.id_token) .field("state", &self.state) .field("timestamp", &self.timestamp) - .field("extra", &self.additional_fields) + .field("additional_fields", &self.additional_fields) .finish() } else { f.debug_struct("AccessToken") @@ -533,7 +392,7 @@ impl fmt::Debug for AccessToken { .field("id_token", &"[REDACTED]") .field("state", &self.state) .field("timestamp", &self.timestamp) - .field("extra", &self.additional_fields) + .field("additional_fields", &self.additional_fields) .finish() } } @@ -541,6 +400,29 @@ impl fmt::Debug for AccessToken { impl AsRef<str> for AccessToken { fn as_ref(&self) -> &str { - self.bearer_token() + self.access_token.as_str() + } +} + +impl<'de> Deserialize<'de> for AccessToken { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let inner_access_token: PhantomAccessToken = Deserialize::deserialize(deserializer)?; + Ok(AccessToken { + access_token: inner_access_token.access_token, + token_type: inner_access_token.token_type, + expires_in: inner_access_token.expires_in.clone(), + ext_expires_in: inner_access_token.ext_expires_in, + scope: inner_access_token.scope, + refresh_token: inner_access_token.refresh_token, + user_id: inner_access_token.user_id, + id_token: inner_access_token.id_token, + state: inner_access_token.state, + timestamp: Some(Utc::now() + Duration::seconds(inner_access_token.expires_in)), + additional_fields: inner_access_token.additional_fields, + log_pii: false, + }) } } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 2be3be2f..38c4e72d 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -899,7 +899,7 @@ impl OAuthSerializer { /// oauth.access_token(access_token); /// ``` pub fn access_token(&mut self, ac: AccessToken) { - if let Some(refresh_token) = ac.refresh_token() { + if let Some(refresh_token) = ac.refresh_token.as_ref() { self.refresh_token(refresh_token.as_str()); } self.access_token.replace(ac); @@ -943,7 +943,7 @@ impl OAuthSerializer { } match self.get_access_token() { - Some(token) => match token.refresh_token() { + Some(token) => match token.refresh_token { Some(t) => Ok(t), None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), }, diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index 870cd379..2232df68 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -131,6 +131,13 @@ impl<'de> Deserialize<'de> for IdToken { formatter.write_str("`code`, `id_token`, `state`, and `session_state`") } + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + IdToken::from_str(v).map_err(|err| Error::custom(err)) + } + fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> where E: serde::de::Error, @@ -158,13 +165,6 @@ impl<'de> Deserialize<'de> for IdToken { } Ok(id_token) } - - fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> - where - E: Error, - { - IdToken::from_str(v).map_err(|err| Error::custom(err)) - } } deserializer.deserialize_identifier(IdTokenVisitor) } diff --git a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs new file mode 100644 index 00000000..47c964ad --- /dev/null +++ b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs @@ -0,0 +1,59 @@ +use crate::access_token::AccessToken; +use crate::identity::{CredentialStore, CredentialStoreType, TokenCacheProviderType}; +use std::collections::BTreeMap; + +#[derive(Clone)] +pub struct InMemoryCredentialStore { + credentials: BTreeMap<String, CredentialStoreType>, +} + +impl InMemoryCredentialStore { + pub fn new() -> InMemoryCredentialStore { + InMemoryCredentialStore { + credentials: BTreeMap::new(), + } + } + + pub fn from_bearer_token<T: AsRef<str>>( + client_id: T, + bearer_token: T, + ) -> InMemoryCredentialStore { + let mut credentials = BTreeMap::new(); + credentials.insert( + client_id.as_ref().to_owned(), + CredentialStoreType::Bearer(bearer_token.as_ref().to_owned()), + ); + InMemoryCredentialStore { credentials } + } + + pub fn from_access_token<T: AsRef<str>>( + client_id: T, + access_token: AccessToken, + ) -> InMemoryCredentialStore { + let mut credentials = BTreeMap::new(); + credentials.insert( + client_id.as_ref().to_owned(), + CredentialStoreType::AccessToken(access_token), + ); + InMemoryCredentialStore { credentials } + } +} + +impl CredentialStore for InMemoryCredentialStore { + fn token_cache_provider(&self) -> TokenCacheProviderType { + TokenCacheProviderType::InMemory + } + + fn get_token_by_client_id(&self, client_id: &str) -> &CredentialStoreType { + info!("InMemoryCredentialStore"); + self.credentials + .get(client_id) + .unwrap_or_else(|| &CredentialStoreType::UnInitialized) + } + + fn update_by_client_id(&mut self, client_id: &str, credential_store_type: CredentialStoreType) { + info!("InMemoryCredentialStore"); + self.credentials + .insert(client_id.to_owned(), credential_store_type); + } +} diff --git a/graph-oauth/src/identity/credential_store/mod.rs b/graph-oauth/src/identity/credential_store/mod.rs new file mode 100644 index 00000000..d81c319e --- /dev/null +++ b/graph-oauth/src/identity/credential_store/mod.rs @@ -0,0 +1,44 @@ +mod in_memory_credential_store; +mod token_cache_providers; + +pub use in_memory_credential_store::*; +pub use token_cache_providers::*; + +use crate::oauth::AccessToken; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum CredentialStoreType { + Bearer(String), + AccessToken(AccessToken), + UnInitialized, +} + +pub trait CredentialStore { + fn token_cache_provider(&self) -> TokenCacheProviderType { + TokenCacheProviderType::UnInitialized + } + + fn get_token(&self) -> &CredentialStoreType { + &CredentialStoreType::UnInitialized + } + + fn update(&mut self, _credential_store_type: CredentialStoreType) {} + + fn get_token_by_client_id(&self, client_id: &str) -> &CredentialStoreType; + + fn update_by_client_id( + &mut self, + _client_id: &str, + _credential_store_type: CredentialStoreType, + ) { + } +} + +pub(crate) struct UnInitializedCredentialStore; + +impl CredentialStore for UnInitializedCredentialStore { + fn get_token_by_client_id(&self, _client_id: &str) -> &CredentialStoreType { + info!("UnInitializedCredentialStore"); + &CredentialStoreType::UnInitialized + } +} diff --git a/graph-oauth/src/identity/credential_store/token_cache_providers.rs b/graph-oauth/src/identity/credential_store/token_cache_providers.rs new file mode 100644 index 00000000..572097d2 --- /dev/null +++ b/graph-oauth/src/identity/credential_store/token_cache_providers.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub enum TokenCacheProviderType { + UnInitialized, + InMemory, + Session, + Distributed, +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index fe431232..c0d42a0f 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, - AzureAuthorityHost, CredentialBuilder, TokenCredentialOptions, TokenRequest, + AzureAuthorityHost, CredentialBuilder, TokenCredential, TokenCredentialOptions, TokenRequest, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; @@ -197,6 +197,12 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { } } +impl TokenCredential for AuthorizationCodeCertificateCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + #[derive(Clone)] pub struct AuthorizationCodeCertificateCredentialBuilder { credential: AuthorizationCodeCertificateCredential, diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 60f7fb8c..342b0462 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,7 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, - CredentialBuilder, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, + CredentialBuilder, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, + TokenRequest, }; use crate::oauth::AuthCodeAuthorizationUrlBuilder; use async_trait::async_trait; @@ -203,6 +204,12 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { } } +impl TokenCredential for AuthorizationCodeCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + #[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { credential: AuthorizationCodeCredential, diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs new file mode 100644 index 00000000..73ab7573 --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_application.rs @@ -0,0 +1,64 @@ +use crate::identity::{CredentialStoreType, TokenRequest}; +use crate::oauth::AccessToken; +use async_trait::async_trait; +use serde_json::from_str; +use serde_urlencoded::from_bytes; + +#[async_trait] +pub trait ClientApplication: TokenRequest { + fn get_credential_from_store(&self) -> &CredentialStoreType; + + fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType); + + fn get_token_credential(&mut self) -> anyhow::Result<CredentialStoreType> { + debug!("get_token_credential"); + let credential_from_store = self.get_credential_from_store(); + if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { + Ok(credential_from_store.clone()) + } else { + let response = self.get_token()?; + let token_value: serde_json::Value = response.json()?; + let bearer = token_value.to_string(); + let access_token_result: serde_json::Result<AccessToken> = + serde_json::from_value(token_value); + match access_token_result { + Ok(access_token) => { + let credential_store_type = CredentialStoreType::AccessToken(access_token); + self.update_token_credential_store(credential_store_type.clone()); + Ok(credential_store_type) + } + Err(_) => { + let credential_store_type = CredentialStoreType::Bearer(bearer); + self.update_token_credential_store(credential_store_type.clone()); + Ok(credential_store_type) + } + } + } + } + + async fn get_token_credential_async(&mut self) -> anyhow::Result<CredentialStoreType> { + debug!("get_token_credential"); + let credential_from_store = self.get_credential_from_store(); + if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { + Ok(credential_from_store.clone()) + } else { + let response = self.get_token_async().await?; + let token_value: serde_json::Value = response.json().await?; + let bearer = token_value.to_string(); + let access_token_result: serde_json::Result<AccessToken> = + serde_json::from_value(token_value); + match access_token_result { + Ok(access_token) => { + let credential_store_type = CredentialStoreType::AccessToken(access_token); + self.update_token_credential_store(credential_store_type.clone()); + Ok(credential_store_type) + } + Err(_) => { + let credential_store_type = CredentialStoreType::Bearer(bearer); + self.update_token_credential_store(credential_store_type.clone()); + Ok(credential_store_type) + } + } + } + } +} diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index c67a2497..46037e68 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, + Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; @@ -153,6 +153,12 @@ impl AuthorizationSerializer for ClientCertificateCredential { } } +impl TokenCredential for ClientCertificateCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + pub struct ClientCertificateCredentialBuilder { credential: ClientCertificateCredential, } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 641478fa..0aa15b24 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, - ClientCredentialsAuthorizationUrlBuilder, CredentialBuilder, TokenRequest, + ClientCredentialsAuthorizationUrlBuilder, CredentialBuilder, TokenCredential, TokenRequest, }; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; @@ -127,6 +127,12 @@ impl AuthorizationSerializer for ClientSecretCredential { } } +impl TokenCredential for ClientSecretCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + pub struct ClientSecretCredentialBuilder { credential: ClientSecretCredential, } @@ -137,7 +143,7 @@ impl ClientSecretCredentialBuilder { credential: ClientSecretCredential { client_id: String::new(), client_secret: String::new(), - scope: vec![], + scope: vec!["https://graph.microsoft.com/.default".into()], authority: Default::default(), token_credential_options: Default::default(), serializer: Default::default(), diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs index c004a555..bb521417 100644 --- a/graph-oauth/src/identity/credentials/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index e284c60a..0d4b0af8 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,8 +1,10 @@ use crate::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - AzureAuthorityHost, ClientCertificateCredential, ClientSecretCredential, - TokenCredentialOptions, TokenRequest, + AzureAuthorityHost, ClientApplication, ClientCertificateCredential, ClientSecretCredential, + CredentialStore, CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, + TokenCacheProviderType, TokenCredential, TokenCredentialOptions, TokenRequest, }; +use crate::oauth::UnInitializedCredentialStore; use async_trait::async_trait; use graph_error::{AuthorizationResult, GraphResult}; use reqwest::header::{HeaderValue, CONTENT_TYPE}; @@ -18,7 +20,8 @@ use wry::http::HeaderMap; pub struct ConfidentialClientApplication { http_client: reqwest::Client, token_credential_options: TokenCredentialOptions, - credential: Box<dyn AuthorizationSerializer + Send>, + credential: Box<dyn TokenCredential + Send>, + credential_store: Box<dyn CredentialStore + Send>, } impl ConfidentialClientApplication { @@ -112,6 +115,40 @@ impl TokenRequest for ConfidentialClientApplication { } } +impl TokenCredential for ConfidentialClientApplication { + fn client_id(&self) -> &String { + self.credential.client_id() + } +} + +impl ClientApplication for ConfidentialClientApplication { + fn get_credential_from_store(&self) -> &CredentialStoreType { + match self.credential_store.token_cache_provider() { + TokenCacheProviderType::UnInitialized => &CredentialStoreType::UnInitialized, + TokenCacheProviderType::InMemory => { + let client_id = self.client_id(); + self.credential_store + .get_token_by_client_id(client_id.as_str()) + } + TokenCacheProviderType::Session => &CredentialStoreType::UnInitialized, + TokenCacheProviderType::Distributed => &CredentialStoreType::UnInitialized, + } + } + + fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType) { + match self.credential_store.token_cache_provider() { + TokenCacheProviderType::UnInitialized => {} + TokenCacheProviderType::InMemory => { + let client_id = self.client_id().clone(); + self.credential_store + .update_by_client_id(client_id.as_str(), credential_store_type); + } + TokenCacheProviderType::Session => {} + TokenCacheProviderType::Distributed => {} + } + } +} + impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCredential) -> Self { ConfidentialClientApplication { @@ -122,6 +159,7 @@ impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), + credential_store: Box::new(UnInitializedCredentialStore), } } } @@ -136,6 +174,7 @@ impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplicat .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), + credential_store: Box::new(UnInitializedCredentialStore), } } } @@ -150,6 +189,7 @@ impl From<ClientSecretCredential> for ConfidentialClientApplication { .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), + credential_store: Box::new(InMemoryCredentialStore::new()), } } } @@ -164,6 +204,22 @@ impl From<ClientCertificateCredential> for ConfidentialClientApplication { .unwrap(), token_credential_options: value.token_credential_options.clone(), credential: Box::new(value), + credential_store: Box::new(UnInitializedCredentialStore), + } + } +} + +impl From<OpenIdCredential> for ConfidentialClientApplication { + fn from(value: OpenIdCredential) -> Self { + ConfidentialClientApplication { + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), + token_credential_options: value.token_credential_options.clone(), + credential: Box::new(value), + credential_store: Box::new(UnInitializedCredentialStore), } } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index ff8a1805..ccc9f9a1 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, TokenCredentialOptions, }; use crate::oauth::DeviceCode; use graph_error::{AuthorizationFailure, AuthorizationResult}; @@ -15,7 +15,7 @@ static DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_c /// and refresh tokens as needed. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code #[derive(Clone)] -pub struct DeviceAuthorizationCredential { +pub struct DeviceCodeCredential { /// Required when requesting a new access token using a refresh token /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. @@ -41,13 +41,13 @@ pub struct DeviceAuthorizationCredential { serializer: OAuthSerializer, } -impl DeviceAuthorizationCredential { +impl DeviceCodeCredential { pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( client_id: T, device_code: T, scope: I, - ) -> DeviceAuthorizationCredential { - DeviceAuthorizationCredential { + ) -> DeviceCodeCredential { + DeviceCodeCredential { refresh_token: None, client_id: client_id.as_ref().to_owned(), device_code: Some(device_code.as_ref().to_owned()), @@ -63,7 +63,7 @@ impl DeviceAuthorizationCredential { } } -impl AuthorizationSerializer for DeviceAuthorizationCredential { +impl AuthorizationSerializer for DeviceCodeCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -158,13 +158,13 @@ impl AuthorizationSerializer for DeviceAuthorizationCredential { #[derive(Clone)] pub struct DeviceCodeCredentialBuilder { - credential: DeviceAuthorizationCredential, + credential: DeviceCodeCredential, } impl DeviceCodeCredentialBuilder { fn new() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { - credential: DeviceAuthorizationCredential { + credential: DeviceCodeCredential { refresh_token: None, client_id: String::new(), device_code: None, @@ -216,7 +216,7 @@ impl DeviceCodeCredentialBuilder { self.credential.token_credential_options = token_credential_options; } - pub fn build(&self) -> DeviceAuthorizationCredential { + pub fn build(&self) -> DeviceCodeCredential { self.credential.clone() } } @@ -224,7 +224,7 @@ impl DeviceCodeCredentialBuilder { impl From<&DeviceCode> for DeviceCodeCredentialBuilder { fn from(value: &DeviceCode) -> Self { DeviceCodeCredentialBuilder { - credential: DeviceAuthorizationCredential { + credential: DeviceCodeCredential { refresh_token: None, client_id: String::new(), device_code: Some(value.device_code.clone()), @@ -244,7 +244,7 @@ mod test { #[test] #[should_panic] fn no_device_code() { - let mut credential = DeviceAuthorizationCredential::builder() + let mut credential = DeviceCodeCredential::builder() .with_client_id("CLIENT_ID") .with_scope(vec!["scope"]) .build(); diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index cc5e2545..279f9403 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -1,8 +1,13 @@ -use crate::identity::{AuthorizationSerializer, ClientSecretCredential}; +use crate::identity::{ + AuthorizationSerializer, AzureAuthorityHost, ClientSecretCredential, TokenCredential, +}; use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, }; +use graph_error::AuthorizationResult; +use std::collections::HashMap; use std::env::VarError; +use url::Url; const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; @@ -11,7 +16,7 @@ const AZURE_USERNAME: &str = "AZURE_USERNAME"; const AZURE_PASSWORD: &str = "AZURE_PASSWORD"; pub struct EnvironmentCredential { - pub credential: Box<dyn AuthorizationSerializer + Send>, + pub credential: Box<dyn TokenCredential + Send>, } impl EnvironmentCredential { @@ -136,6 +141,16 @@ impl EnvironmentCredential { } } +impl AuthorizationSerializer for EnvironmentCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + self.credential.uri(azure_authority_host) + } + + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form_urlencode() + } +} + impl From<ClientSecretCredential> for EnvironmentCredential { fn from(value: ClientSecretCredential) -> Self { EnvironmentCredential { diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 612f6014..13633c82 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -5,6 +5,7 @@ mod as_query; mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; +mod client_application; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; @@ -26,6 +27,7 @@ mod resource_owner_password_credential; mod response_mode; mod response_type; mod token_credential; +mod token_credential_options; mod token_flow_authorization_url; mod token_request; @@ -36,6 +38,7 @@ pub use as_query::*; pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; +pub use client_application::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; @@ -58,6 +61,7 @@ pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; pub use token_credential::*; +pub use token_credential_options::*; pub use token_flow_authorization_url::*; pub use token_request::*; diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 78419666..07f4e86a 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,7 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, - OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredentialOptions, TokenRequest, + OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, + TokenRequest, }; use crate::oauth::OpenIdAuthorizationUrlBuilder; use async_trait::async_trait; @@ -198,6 +199,12 @@ impl AuthorizationSerializer for OpenIdCredential { } } +impl TokenCredential for OpenIdCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + #[derive(Clone)] pub struct OpenIdCredentialBuilder { credential: OpenIdCredential, diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 91c45852..891bab1d 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,5 +1,5 @@ use crate::identity::{ - AuthorizationSerializer, AzureAuthorityHost, ResourceOwnerPasswordCredential, + AuthorizationSerializer, AzureAuthorityHost, ResourceOwnerPasswordCredential, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; @@ -18,7 +18,7 @@ use url::Url; pub struct PublicClientApplication { http_client: reqwest::Client, token_credential_options: TokenCredentialOptions, - credential: Box<dyn AuthorizationSerializer + Send>, + credential: Box<dyn TokenCredential + Send>, } impl PublicClientApplication { @@ -111,6 +111,12 @@ impl TokenRequest for PublicClientApplication { } } +impl TokenCredential for PublicClientApplication { + fn client_id(&self) -> &String { + self.credential.client_id() + } +} + impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { fn from(value: ResourceOwnerPasswordCredential) -> Self { PublicClientApplication { diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 6f61eded..58fb3a38 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,7 +1,9 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, + TokenCredentialOptions, TokenRequest, }; +use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -66,6 +68,17 @@ impl ResourceOwnerPasswordCredential { serializer: Default::default(), } } + + pub fn builder() -> ResourceOwnerPasswordCredentialBuilder { + ResourceOwnerPasswordCredentialBuilder::new() + } +} + +#[async_trait] +impl TokenRequest for ResourceOwnerPasswordCredential { + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } } impl AuthorizationSerializer for ResourceOwnerPasswordCredential { @@ -108,13 +121,19 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { } } +impl TokenCredential for ResourceOwnerPasswordCredential { + fn client_id(&self) -> &String { + &self.client_id + } +} + #[derive(Clone)] pub struct ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential, } impl ResourceOwnerPasswordCredentialBuilder { - pub fn new() -> ResourceOwnerPasswordCredentialBuilder { + fn new() -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { client_id: String::new(), diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 2b2b0cfc..3cae3526 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -1,6 +1,5 @@ -use crate::identity::AzureAuthorityHost; +use crate::identity::{AuthorizationSerializer, CredentialStoreType, TokenRequest}; -#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct TokenCredentialOptions { - pub(crate) azure_authority_host: AzureAuthorityHost, +pub trait TokenCredential: AuthorizationSerializer + TokenRequest { + fn client_id(&self) -> &String; } diff --git a/graph-oauth/src/identity/credentials/token_credential_options.rs b/graph-oauth/src/identity/credentials/token_credential_options.rs new file mode 100644 index 00000000..0ae29b66 --- /dev/null +++ b/graph-oauth/src/identity/credentials/token_credential_options.rs @@ -0,0 +1,21 @@ +use crate::identity::AzureAuthorityHost; +use reqwest::header::HeaderMap; +use std::collections::HashMap; + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct TokenCredentialOptions { + pub(crate) azure_authority_host: AzureAuthorityHost, + + pub extra_query_parameters: HashMap<String, String>, + + pub extra_header_parameters: HeaderMap, + + /// Specifies if the token request will ignore the access token in the token cache + /// and will attempt to acquire a new access token. + pub force_refresh: bool, + + /// Enables to override the tenant/account for which to get a token. + /// This is useful in multi-tenant apps in the cases where a given user account is a guest + /// in other tenants, and you want to acquire tokens for a specific tenant. + pub tenant: Option<String>, +} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index d48fe318..7af63775 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,12 +1,14 @@ mod allowed_host_validator; mod authority; mod authorization_serializer; +mod credential_store; mod credentials; pub(crate) mod form_credential; pub use allowed_host_validator::*; pub use authority::*; pub use authorization_serializer::*; +pub use credential_store::*; pub use credentials::*; #[cfg(feature = "openssl")] diff --git a/src/client/graph.rs b/src/client/graph.rs index cd69f0cc..9aec4c51 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -528,7 +528,7 @@ impl From<String> for Graph { impl From<&AccessToken> for Graph { fn from(token: &AccessToken) -> Self { - Graph::new(token.bearer_token()) + Graph::new(token.access_token.as_str()) } } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 1739ffd4..f696d4b9 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -2,7 +2,10 @@ use from_as::*; use graph_core::resource::ResourceIdentity; -use graph_rs_sdk::oauth::{AccessToken, ClientSecretCredential, OAuthSerializer}; +use graph_rs_sdk::oauth::{ + AccessToken, AuthorizationCodeCredential, ClientSecretCredential, CredentialBuilder, + OAuthSerializer, ResourceOwnerPasswordCredential, TokenRequest, +}; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -119,38 +122,23 @@ impl OAuthTestCredentials { } } - pub fn new_local() -> OAuthTestCredentials { - OAuthTestCredentials::new_local_from_path("./env.toml") - } - - pub fn new_local_from_path(path: &str) -> OAuthTestCredentials { - let mut creds: OAuthTestCredentials = OAuthTestCredentials::from_file(path).unwrap(); - creds - .scope - .push("https://graph.microsoft.com/.default".into()); - creds - } - - fn into_oauth(self) -> OAuthSerializer { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id(self.client_id.as_str()) - .client_secret(self.client_secret.as_str()) - .username(self.username.as_str()) - .password(self.password.as_str()) - .add_scope("https://graph.microsoft.com/.default") - .access_token_url( - format!( - "https://login.microsoftonline.com/{}/oauth2/v2.0/token", - self.tenant.as_str() - ) - .as_str(), - ); - oauth - } - fn client_credentials(self) -> ClientSecretCredential { - ClientSecretCredential::new(self.client_id.as_str(), self.client_secret.as_str()) + ClientSecretCredential::builder() + .with_client_secret(self.client_secret.as_str()) + .with_client_id(self.client_id.as_str()) + .with_tenant(self.tenant.as_str()) + .with_scope(vec!["https://graph.microsoft.com/.default"]) + .build() + } + + fn resource_owner_password_credential(self) -> ResourceOwnerPasswordCredential { + ResourceOwnerPasswordCredential::builder() + .with_tenant(self.tenant.as_str()) + .with_client_id(self.client_id.as_str()) + .with_username(self.username.as_str()) + .with_password(self.password.as_str()) + .with_scope(vec!["https://graph.microsoft.com/.default"]) + .build() } } @@ -164,51 +152,60 @@ pub enum OAuthTestClient { impl OAuthTestClient { fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, AccessToken)> { let user_id = creds.user_id.clone()?; - let mut oauth: OAuthSerializer = creds.into_oauth(); - let mut req = { - match self { - OAuthTestClient::ClientCredentials => oauth.build().client_credentials(), - OAuthTestClient::ResourceOwnerPasswordCredentials => { - oauth.build().resource_owner_password_credentials() + match self { + OAuthTestClient::ClientCredentials => { + let mut credential = creds.client_credentials(); + if let Ok(response) = credential.get_token() { + let token: AccessToken = response.json().unwrap(); + Some((user_id, token)) + } else { + None } - OAuthTestClient::AuthorizationCodeCredential => { - oauth.build().authorization_code_grant() + } + OAuthTestClient::ResourceOwnerPasswordCredentials => { + let mut credential = creds.resource_owner_password_credential(); + if let Ok(response) = credential.get_token() { + let token: AccessToken = response.json().unwrap(); + Some((user_id, token)) + } else { + None } } - }; - - if let Ok(response) = req.access_token().send() { - let token: AccessToken = response.json().unwrap(); - Some((user_id, token)) - } else { - None + _ => None, } } + pub fn get_client_credentials(&self, creds: OAuthTestCredentials) -> ClientSecretCredential { + creds.client_credentials() + } + async fn get_access_token_async( &self, creds: OAuthTestCredentials, ) -> Option<(String, AccessToken)> { let user_id = creds.user_id.clone()?; - let mut oauth: OAuthSerializer = creds.into_oauth(); - let mut req = { - match self { - OAuthTestClient::ClientCredentials => oauth.build_async().client_credentials(), - OAuthTestClient::ResourceOwnerPasswordCredentials => { - oauth.build_async().resource_owner_password_credentials() - } - OAuthTestClient::AuthorizationCodeCredential => { - oauth.build_async().authorization_code_grant() + match self { + OAuthTestClient::ClientCredentials => { + let mut credential = creds.client_credentials(); + match credential.get_token_async().await { + Ok(response) => { + let token: AccessToken = response.json().await.unwrap(); + Some((user_id, token)) + } + Err(_) => None, } } - }; - - match req.access_token().send().await { - Ok(response) => { - let token: AccessToken = response.json().await.unwrap(); - Some((user_id, token)) + OAuthTestClient::ResourceOwnerPasswordCredentials => { + let mut credential = creds.resource_owner_password_credential(); + match credential.get_token_async().await { + Ok(response) => { + let token: AccessToken = response.json().await.unwrap(); + Some((user_id, token)) + } + Err(_) => None, + } } - Err(_) => None, + _ => None, } } @@ -261,12 +258,21 @@ impl OAuthTestClient { let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token(credentials) { - Some((id, Graph::new(token.bearer_token()))) + Some((id, Graph::new(token.access_token.as_str()))) } else { None } } + pub fn client_credentials_by_rid( + resource_identity: ResourceIdentity, + ) -> Option<ClientSecretCredential> { + let app_registration = OAuthTestClient::get_app_registration()?; + let client = app_registration.get_by_resource_identity(resource_identity)?; + let (test_client, credentials) = client.default_client()?; + Some(test_client.get_client_credentials(credentials)) + } + pub async fn graph_by_rid_async( resource_identity: ResourceIdentity, ) -> Option<(String, Graph)> { @@ -274,7 +280,7 @@ impl OAuthTestClient { let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token_async(credentials).await { - Some((id, Graph::new(token.bearer_token()))) + Some((id, Graph::new(token.access_token.as_str()))) } else { None } @@ -282,7 +288,7 @@ impl OAuthTestClient { pub fn graph(&self) -> Option<(String, Graph)> { if let Some((id, token)) = self.request_access_token() { - Some((id, Graph::new(token.bearer_token()))) + Some((id, Graph::new(token.access_token.as_str()))) } else { None } @@ -290,7 +296,7 @@ impl OAuthTestClient { pub async fn graph_async(&self) -> Option<(String, Graph)> { if let Some((id, token)) = self.request_access_token_async().await { - Some((id, Graph::new(token.bearer_token()))) + Some((id, Graph::new(token.access_token.as_str()))) } else { None } diff --git a/tests/access_token_request.rs b/tests/access_token_request.rs index dc4528c0..7b24d1f3 100644 --- a/tests/access_token_request.rs +++ b/tests/access_token_request.rs @@ -6,6 +6,6 @@ use test_tools::oauth_request::OAuthTestClient; #[test] fn client_credentials_test() { if let Some(token) = OAuthTestClient::ClientCredentials.request_access_token() { - assert!(!token.1.bearer_token().is_empty()); + assert!(!token.1.access_token.is_empty()); } } diff --git a/tests/access_token_tests.rs b/tests/access_token_tests.rs index bb0c43fe..fbf08ed4 100644 --- a/tests/access_token_tests.rs +++ b/tests/access_token_tests.rs @@ -2,36 +2,6 @@ use graph_oauth::oauth::AccessToken; use std::thread; use std::time::Duration; -#[test] -fn get_method() { - let mut access_token = AccessToken::default(); - access_token - .set_expires_in(3600) - .set_token_type("bearer") - .set_bearer_token("ASODFIUJ34KJ;LADSK") - .set_scope("offline") - .set_refresh_token("eyJh...9323"); - assert_eq!(access_token.expires_in(), 3600); - assert_eq!(access_token.token_type(), "bearer"); - assert_eq!(access_token.bearer_token(), "ASODFIUJ34KJ;LADSK"); - assert_eq!(access_token.scopes(), Some(&"offline".into())); - assert_eq!( - access_token.refresh_token(), - Some("eyJh...9323".to_string()) - ); -} - -#[test] -fn access_token_field_encoding() { - // Internally this is base64. - let mut access_token = AccessToken::default(); - access_token.set_bearer_token("ASDFJ;34LIUASDOFI NASDOFIUY OP"); - assert_eq!( - "ASDFJ;34LIUASDOFI NASDOFIUY OP", - access_token.bearer_token() - ); -} - #[test] fn is_expired_test() { let mut access_token = AccessToken::default(); diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index 6bf75478..af72ccdc 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -1,3 +1,4 @@ +use graph_core::resource::ResourceIdentity; use graph_http::api_impl::ODataQuery; use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; use test_tools::oauth_request::{Environment, OAuthTestClient}; @@ -6,7 +7,9 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; async fn get_drafts_mail_folder() { if Environment::is_local() { let _ = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + if let Some((id, client)) = + OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await + { let response = client .user(id.as_str()) .mail_folder("drafts") @@ -27,7 +30,9 @@ async fn get_drafts_mail_folder() { async fn mail_folder_list_messages() { if Environment::is_local() { let _ = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + if let Some((id, client)) = + OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await + { let response = client .user(id.as_str()) .mail_folder("inbox") @@ -40,6 +45,7 @@ async fn mail_folder_list_messages() { assert!(response.status().is_success()); let body: serde_json::Value = response.json().await.unwrap(); + dbg!(&body); let messages = body["value"].as_array().unwrap(); assert_eq!(messages.len(), 2); } diff --git a/tests/onenote_request.rs b/tests/onenote_request.rs index 98711007..a259b78f 100644 --- a/tests/onenote_request.rs +++ b/tests/onenote_request.rs @@ -1,3 +1,4 @@ +use graph_core::resource::ResourceIdentity; use graph_http::traits::ResponseExt; use graph_rs_sdk::header::{HeaderValue, CONTENT_TYPE}; use graph_rs_sdk::http::FileConfig; @@ -15,7 +16,8 @@ async fn list_get_notebooks_and_sections() { } let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await + { let notebooks = client .user(id.as_str()) .onenote() @@ -78,12 +80,13 @@ async fn list_get_notebooks_and_sections() { #[tokio::test] async fn create_delete_page_from_file() { - if Environment::is_appveyor() { + if Environment::is_appveyor() || Environment::is_local() { return; } let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await + { let res = client .user(&id) .onenote() @@ -117,12 +120,14 @@ async fn create_delete_page_from_file() { #[tokio::test] async fn download_page() { - if Environment::is_appveyor() { + if Environment::is_appveyor() || Environment::is_local() { return; } let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((user_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + if let Some((user_id, client)) = + OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await + { let file_location = "./test_files/downloaded_page.html"; let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); clean_up.rm_files(file_location.into()); From c27d5648b6576db573fdfb8afb49ff264dedb73a Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 27 May 2023 21:44:28 -0400 Subject: [PATCH 021/118] Fix clippy lints --- examples/oauth_certificate/main.rs | 2 +- graph-oauth/src/access_token.rs | 18 +++++++++--------- .../in_memory_credential_store.rs | 4 ++-- .../src/identity/credential_store/mod.rs | 1 + .../identity/credentials/client_application.rs | 2 -- .../credentials/code_flow_credential.rs | 2 +- .../credentials/device_code_credential.rs | 2 +- .../identity/credentials/token_credential.rs | 2 +- test-tools/src/oauth_request.rs | 4 ++-- 9 files changed, 18 insertions(+), 19 deletions(-) diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index f501459c..3cd560fc 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -125,7 +125,7 @@ async fn handle_redirect( let access_token: AccessToken = response.json().await.unwrap(); // If all went well here we can print out the Access Token. - println!("AccessToken: {:#?}", access_token.bearer_token()); + println!("AccessToken: {:#?}", access_token.access_token); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 8ccdd901..33e9b5d7 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -1,6 +1,6 @@ use crate::id_token::IdToken; -use crate::jwt::{Claim, JsonWebToken, JwtParser}; -use chrono::{DateTime, Duration, LocalResult, TimeZone, Utc}; +use crate::jwt::{JsonWebToken, JwtParser}; +use chrono::{DateTime, Duration, Utc}; use chrono_humanize::HumanTime; use graph_error::GraphFailure; use serde::{Deserialize, Deserializer}; @@ -8,7 +8,7 @@ use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; -use std::fmt::format; + use std::str::FromStr; // Used to set timestamp based on expires in @@ -81,8 +81,8 @@ impl AccessToken { pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> AccessToken { AccessToken { token_type: token_type.into(), - ext_expires_in: Some(expires_in.clone()), - expires_in: expires_in.clone(), + ext_expires_in: Some(expires_in), + expires_in, scope: Some(scope.into()), access_token: access_token.into(), refresh_token: None, @@ -119,7 +119,7 @@ impl AccessToken { /// access_token.set_expires_in(3600); /// ``` pub fn set_expires_in(&mut self, expires_in: i64) -> &mut AccessToken { - self.expires_in = expires_in.clone(); + self.expires_in = expires_in; self.timestamp = Some(Utc::now() + Duration::seconds(expires_in)); self } @@ -264,7 +264,7 @@ impl AccessToken { /// // The timestamp is in UTC. /// ``` pub fn gen_timestamp(&mut self) { - self.timestamp = Some(Utc::now() + Duration::seconds(self.expires_in.clone())); + self.timestamp = Some(Utc::now() + Duration::seconds(self.expires_in)); } /// Check whether the access token is expired. Uses the expires_in @@ -344,7 +344,7 @@ impl TryFrom<reqwest::blocking::RequestBuilder> for AccessToken { fn try_from(value: reqwest::blocking::RequestBuilder) -> Result<Self, Self::Error> { let response = value.send()?; - Ok(AccessToken::try_from(response)?) + AccessToken::try_from(response) } } @@ -413,7 +413,7 @@ impl<'de> Deserialize<'de> for AccessToken { Ok(AccessToken { access_token: inner_access_token.access_token, token_type: inner_access_token.token_type, - expires_in: inner_access_token.expires_in.clone(), + expires_in: inner_access_token.expires_in, ext_expires_in: inner_access_token.ext_expires_in, scope: inner_access_token.scope, refresh_token: inner_access_token.refresh_token, diff --git a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs index 47c964ad..df084ea7 100644 --- a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs +++ b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs @@ -2,7 +2,7 @@ use crate::access_token::AccessToken; use crate::identity::{CredentialStore, CredentialStoreType, TokenCacheProviderType}; use std::collections::BTreeMap; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct InMemoryCredentialStore { credentials: BTreeMap<String, CredentialStoreType>, } @@ -48,7 +48,7 @@ impl CredentialStore for InMemoryCredentialStore { info!("InMemoryCredentialStore"); self.credentials .get(client_id) - .unwrap_or_else(|| &CredentialStoreType::UnInitialized) + .unwrap_or(&CredentialStoreType::UnInitialized) } fn update_by_client_id(&mut self, client_id: &str, credential_store_type: CredentialStoreType) { diff --git a/graph-oauth/src/identity/credential_store/mod.rs b/graph-oauth/src/identity/credential_store/mod.rs index d81c319e..ec92084c 100644 --- a/graph-oauth/src/identity/credential_store/mod.rs +++ b/graph-oauth/src/identity/credential_store/mod.rs @@ -7,6 +7,7 @@ pub use token_cache_providers::*; use crate::oauth::AccessToken; #[derive(Debug, Clone, Eq, PartialEq)] +#[allow(clippy::large_enum_variant)] pub enum CredentialStoreType { Bearer(String), AccessToken(AccessToken), diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs index 73ab7573..3f54e0fe 100644 --- a/graph-oauth/src/identity/credentials/client_application.rs +++ b/graph-oauth/src/identity/credentials/client_application.rs @@ -1,8 +1,6 @@ use crate::identity::{CredentialStoreType, TokenRequest}; use crate::oauth::AccessToken; use async_trait::async_trait; -use serde_json::from_str; -use serde_urlencoded::from_bytes; #[async_trait] pub trait ClientApplication: TokenRequest { diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/code_flow_credential.rs index bb521417..c004a555 100644 --- a/graph-oauth/src/identity/credentials/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/code_flow_credential.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential}; +use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index ccc9f9a1..ae0b37cc 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, TokenCredentialOptions, + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; use crate::oauth::DeviceCode; use graph_error::{AuthorizationFailure, AuthorizationResult}; diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 3cae3526..9c906bb7 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -1,4 +1,4 @@ -use crate::identity::{AuthorizationSerializer, CredentialStoreType, TokenRequest}; +use crate::identity::{AuthorizationSerializer, TokenRequest}; pub trait TokenCredential: AuthorizationSerializer + TokenRequest { fn client_id(&self) -> &String; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index f696d4b9..bd6270e7 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCredential, ClientSecretCredential, CredentialBuilder, - OAuthSerializer, ResourceOwnerPasswordCredential, TokenRequest, + AccessToken, ClientSecretCredential, CredentialBuilder, ResourceOwnerPasswordCredential, + TokenRequest, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; From 9204b8cfc40ada99a4b5cba812c6984dd0693671 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 29 May 2023 20:22:07 -0400 Subject: [PATCH 022/118] Updates to oauth flows and added helper methods --- examples/oauth/implicit_grant.rs | 2 +- examples/oauth/main.rs | 28 +-- examples/oauth/open_id_connect.rs | 135 ++++++++----- graph-error/src/authorization_failure.rs | 24 ++- graph-oauth/src/auth.rs | 26 ++- .../src/identity/credentials/as_query.rs | 30 +++ .../auth_code_authorization_url.rs | 2 +- ...thorization_code_certificate_credential.rs | 26 ++- .../client_credentials_authorization_url.rs | 2 +- .../src/identity/credentials/crypto.rs | 14 +- .../credentials/device_code_credential.rs | 31 +-- .../implicit_credential_authorization_url.rs | 9 +- .../credentials/open_id_authorization_url.rs | 190 ++++++++++++++---- .../credentials/open_id_credential.rs | 8 +- .../src/identity/credentials/response_type.rs | 49 ++++- .../token_flow_authorization_url.rs | 4 +- 16 files changed, 421 insertions(+), 159 deletions(-) diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index b6498f7d..31e16c9f 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -51,7 +51,7 @@ fn multi_response_types() { // Or let _ = ImplicitCredential::builder() - .with_response_type(ResponseType::FromString(vec![ + .with_response_type(ResponseType::StringSet(vec![ "token".to_string(), "id_token".to_string(), ])) diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 20aa5fdb..3877220a 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -37,22 +37,26 @@ use graph_rs_sdk::oauth::{ #[tokio::main] async fn main() { - // Some examples of what you can use for authentication and getting access tokens. There are - // more ways to perform oauth authorization. + open_id_connect::start_server_main().await; +} - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow - auth_code_grant::start_server_main().await; - auth_code_grant_pkce::start_server_main().await; +/* + // Some examples of what you can use for authentication and getting access tokens. There are + // more ways to perform oauth authorization. - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow - client_credentials_admin_consent::start_server_main().await; + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow + auth_code_grant::start_server_main().await; + auth_code_grant_pkce::start_server_main().await; - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code - device_code::device_code(); + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow + client_credentials_admin_consent::start_server_main().await; - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc - open_id_connect::start_server_main().await; -} + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code + device_code::device_code(); + + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc + open_id_connect::start_server_main().await; +*/ // Quick Examples diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index bad0f460..81ac7809 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,4 +1,7 @@ +use graph_oauth::identity::{CredentialBuilder, ResponseType, TokenRequest}; +use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; +use url::Url; /// # Example /// ``` /// use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuth}; @@ -15,62 +18,66 @@ use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; /// OAuth-enabled applications by using a security token called an ID token. use warp::Filter; +// "client_id": "e0951f73-cafa-455f-9365-50dfd22f56b6", +// "client_secret": "rUWHfYygz~IZH~7I~2.w1-Sedf~T16g8OR", + // The client id and client secret must be changed before running this example. -static CLIENT_ID: &str = "<YOUR_CLIENT_ID>"; -static CLIENT_SECRET: &str = "<YOUR_CLIENT_SECRET>"; +static CLIENT_ID: &str = "e0951f73-cafa-455f-9365-50dfd22f56b6"; +static CLIENT_SECRET: &str = "rUWHfYygz~IZH~7I~2.w1-Sedf~T16g8OR"; -fn oauth_open_id() -> OAuthSerializer { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id(CLIENT_ID) - .client_secret(CLIENT_SECRET) - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .redirect_uri("http://localhost:8000/redirect") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .response_type("id_token code") - .response_mode("form_post") - .add_scope("openid") - .add_scope("Files.Read") - .add_scope("Files.ReadWrite") - .add_scope("Files.Read.All") - .add_scope("Files.ReadWrite.All") - .add_scope("offline_access") - .nonce("7362CAEA-9CA5") - .prompt("login") - .state("12345"); - oauth +fn open_id_credential( + authorization_code: &str, + client_id: &str, + client_secret: &str, +) -> anyhow::Result<OpenIdCredential> { + Ok(OpenIdCredential::builder() + .with_authorization_code(authorization_code) + .with_client_id(client_id) + .with_client_secret(client_secret) + .with_redirect_uri("http://localhost:8000")? + .with_scope(vec!["offline_access", "Files.Read"]) // OpenIdCredential automatically sets the openid scope + .build()) } -async fn handle_redirect(id_token: IdToken) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - println!("Received IdToken: {id_token:#?}"); - - let mut oauth = oauth_open_id(); - - // Pass the id token to the oauth client. - oauth.id_token(id_token); +fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { + Ok(OpenIdCredential::authorization_url_builder() + .new_with_secure_nonce()? + .with_client_id(client_id) + .with_default_scope()? + .extend_scope(vec!["Files.Read"]) + .build() + .url()?) +} +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct OpenIdResponse { + pub code: String, + pub id_token: String, + pub session_state: String, +} - // Build the request to get an access token using open id connect. - let mut request = oauth.build_async().open_id_connect(); +async fn handle_redirect( + id_token: OpenIdResponse, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + println!("Received IdToken: {id_token:#?}"); + let code = id_token.code.clone(); - // Request an access token. - let response = request.access_token().send().await.unwrap(); - println!("{response:#?}"); + let mut credential = open_id_credential(code.as_ref(), CLIENT_ID, CLIENT_SECRET).unwrap(); + let mut result = credential.get_token_async().await; - if response.status().is_success() { - let access_token: AccessToken = response.json().await.unwrap(); + dbg!(&result); - // You can optionally pass the access token to the oauth client in order - // to use a refresh token to get more access tokens. The refresh token - // is stored in AccessToken. - oauth.access_token(access_token); + if let Ok(response) = result { + if response.status().is_success() { + let mut access_token: AccessToken = response.json().await.unwrap(); + access_token.enable_pii_logging(true); - // If all went well here we can print out the OAuth config with the Access Token. - println!("OAuth:\n{:#?}\n", &oauth); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); + // If all went well here we can print out the OAuth config with the Access Token. + println!("\n{:#?}\n", access_token); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + println!("{result:#?}"); + } } // Generic login page response. @@ -94,10 +101,36 @@ pub async fn start_server_main() { .and(warp::body::json()) .and_then(handle_redirect); - // Get the oauth client and request a browser sign in. - let mut oauth = oauth_open_id(); - let mut request = oauth.build_async().open_id_connect(); - request.browser_authorization().open().unwrap(); + std::env::set_var("RUST_LOG", "trace"); + std::env::set_var("GRAPH_TEST_ENV", "true"); + + let url = open_id_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); + webbrowser::open(url.as_ref()); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } + +/* +fn oauth_open_id() -> OAuthSerializer { + let mut oauth = OAuthSerializer::new(); + oauth + .client_id(CLIENT_ID) + .client_secret(CLIENT_SECRET) + .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") + .redirect_uri("http://localhost:8000/redirect") + .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") + .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") + .response_type("id_token code") + .response_mode("form_post") + .add_scope("openid") + .add_scope("Files.Read") + .add_scope("Files.ReadWrite") + .add_scope("Files.Read.All") + .add_scope("Files.ReadWrite.All") + .add_scope("offline_access") + .nonce("7362CAEA-9CA5") + .prompt("login") + .state("12345"); + oauth +} + */ diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 21050970..0bd8721e 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -12,10 +12,21 @@ pub enum AuthorizationFailure { #[error("{0:#?}")] UrlParseError(#[from] url::ParseError), + + #[error("{0:#?}")] + Unknown(String), } impl AuthorizationFailure { - pub fn err<T: AsRef<str>>(name: T) -> AuthorizationFailure { + pub fn unknown<T: ToString>(value: T) -> AuthorizationFailure { + AuthorizationFailure::Unknown(value.to_string()) + } + + pub fn unknown_result<T: ToString>(value: T) -> AuthorizationResult<AuthorizationFailure> { + Err(AuthorizationFailure::Unknown(value.to_string())) + } + + pub fn required<T: AsRef<str>>(name: T) -> AuthorizationFailure { AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), message: None, @@ -36,6 +47,13 @@ impl AuthorizationFailure { } } + pub fn msg_internal_err<T: AsRef<str>>(name: T) -> AuthorizationFailure { + AuthorizationFailure::RequiredValue { + name: name.as_ref().to_owned(), + message: Some("Internal error please file an issue on GitHub https://github.com/sreeise/graph-rs-sdk/issues".to_owned()), + } + } + pub fn msg_result<T>( name: impl AsRef<str>, message: impl ToString, @@ -46,6 +64,10 @@ impl AuthorizationFailure { }) } + pub fn msg_internal_result<T>(name: impl AsRef<str>) -> Result<T, AuthorizationFailure> { + Err(AF::msg_internal_err(name)) + } + pub fn url_parse_error<T>(url_parse_error: url::ParseError) -> Result<T, AuthorizationFailure> { Err(AuthorizationFailure::UrlParseError(url_parse_error)) } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 38c4e72d..50245cc6 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -2,7 +2,8 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; use crate::identity::form_credential::ParameterIs; -use crate::identity::{Authority, AzureAuthorityHost}; +use crate::identity::{AsQuery, Authority, AzureAuthorityHost, Prompt}; +use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; @@ -474,6 +475,13 @@ impl OAuthSerializer { self.insert(OAuthParameter::ResponseType, value) } + pub fn response_types( + &mut self, + value: std::collections::btree_set::Iter<'_, ResponseType>, + ) -> &mut OAuthSerializer { + self.insert(OAuthParameter::ResponseType, value.as_query()) + } + /// Set the nonce. /// /// # Example @@ -487,8 +495,6 @@ impl OAuthSerializer { self.insert(OAuthParameter::Nonce, value) } - // rand = "0.8.5" - /// Set the prompt for open id. /// /// # Example @@ -502,6 +508,10 @@ impl OAuthSerializer { self.insert(OAuthParameter::Prompt, value) } + pub fn prompts(&mut self, value: &[Prompt]) -> &mut OAuthSerializer { + self.insert(OAuthParameter::Prompt, value.to_vec().as_query()) + } + /// Set id token for open id. /// /// # Example @@ -1027,7 +1037,7 @@ impl OAuthSerializer { } pub fn ok_or(&self, oac: &OAuthParameter) -> AuthorizationResult<String> { - self.get(*oac).ok_or(AuthorizationFailure::err(oac)) + self.get(*oac).ok_or(AuthorizationFailure::required(oac)) } pub fn try_as_tuple(&self, oac: &OAuthParameter) -> AuthorizationResult<(String, String)> { @@ -1036,7 +1046,7 @@ impl OAuthSerializer { } else { Ok(( oac.alias().to_owned(), - self.get(*oac).ok_or(AuthorizationFailure::err(oac))?, + self.get(*oac).ok_or(AuthorizationFailure::required(oac))?, )) } } @@ -1074,7 +1084,7 @@ impl OAuthSerializer { } else { let value = self .get(parameter) - .ok_or(AuthorizationFailure::err(parameter))?; + .ok_or(AuthorizationFailure::required(parameter))?; encoder.append_pair(parameter.alias(), value.as_str()); } @@ -1106,7 +1116,7 @@ impl OAuthSerializer { encoder.append_pair("scope", self.join_scopes(" ").as_str()); } } else { - let value = self.get(*oac).ok_or(AuthorizationFailure::err(oac))?; + let value = self.get(*oac).ok_or(AuthorizationFailure::required(oac))?; encoder.append_pair(oac.alias(), value.as_str()); } @@ -1172,7 +1182,7 @@ impl OAuthSerializer { ParameterIs::Required(oac) => { let val = self .get(*oac) - .ok_or(AuthorizationFailure::err(oac.alias()))?; + .ok_or(AuthorizationFailure::required(oac.alias()))?; if val.trim().is_empty() { return AuthorizationFailure::msg_result(oac, "Value cannot be empty"); } else { diff --git a/graph-oauth/src/identity/credentials/as_query.rs b/graph-oauth/src/identity/credentials/as_query.rs index ecdd897e..5db21333 100644 --- a/graph-oauth/src/identity/credentials/as_query.rs +++ b/graph-oauth/src/identity/credentials/as_query.rs @@ -1,3 +1,33 @@ pub trait AsQuery<RHS = Self> { fn as_query(&self) -> String; } + +impl<T: ToString + Clone> AsQuery for std::slice::Iter<'_, T> { + fn as_query(&self) -> String { + self.clone() + .into_iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + } +} + +impl<T: ToString + Clone> AsQuery for std::collections::hash_set::Iter<'_, T> { + fn as_query(&self) -> String { + self.clone() + .into_iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + } +} + +impl<T: ToString + Clone> AsQuery for std::collections::btree_set::Iter<'_, T> { + fn as_query(&self) -> String { + self.clone() + .into_iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + } +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 1089eded..7cf7c0f0 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -306,7 +306,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { let authorization_url = serializer .get(OAuthParameter::AuthorizationUrl) - .ok_or(AF::msg_err("authorization_url", "Internal Error"))?; + .ok_or(AF::msg_internal_err("authorization_url"))?; let mut url = Url::parse(authorization_url.as_str())?; url.set_query(Some(encoder.finish().as_str())); Ok(url) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index c0d42a0f..28113381 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -39,7 +39,7 @@ pub struct AuthorizationCodeCertificateCredential { /// The redirect_uri of your app, where authentication responses can be sent and received /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. - pub(crate) redirect_uri: Url, + pub(crate) redirect_uri: Option<Url>, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. @@ -68,15 +68,21 @@ impl AuthorizationCodeCertificateCredential { client_id: T, authorization_code: T, client_assertion: T, - redirect_uri: U, + redirect_uri: Option<U>, ) -> AuthorizationResult<AuthorizationCodeCertificateCredential> { - let redirect_uri_result = Url::parse(redirect_uri.as_str()); + let redirect_uri = { + if let Some(redirect_uri) = redirect_uri { + redirect_uri.into_url().ok() + } else { + None + } + }; Ok(AuthorizationCodeCertificateCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, + redirect_uri, code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), @@ -111,7 +117,7 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { let uri = self .serializer .get(OAuthParameter::AccessTokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + .ok_or(AF::msg_internal_err("access_token_url"))?; Url::parse(uri.as_str()).map_err(AF::from) } @@ -130,11 +136,14 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { self.serializer .client_id(self.client_id.as_str()) - .redirect_uri(self.redirect_uri.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .extend_scopes(self.scope.clone()); + if let Some(redirect_uri) = self.redirect_uri.as_ref() { + self.serializer.redirect_uri(redirect_uri.as_str()); + } + if let Some(code_verifier) = self.code_verifier.as_ref() { self.serializer.code_verifier(code_verifier.as_ref()); } @@ -215,8 +224,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { authorization_code: None, refresh_token: None, client_id: String::with_capacity(32), - redirect_uri: Url::parse("http://localhost") - .expect("Internal Error - please report"), + redirect_uri: None, code_verifier: None, client_assertion_type: String::new(), client_assertion: CLIENT_ASSERTION_TYPE.to_owned(), @@ -240,7 +248,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { } pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { - self.credential.redirect_uri = redirect_uri.into_url()?; + self.credential.redirect_uri = Some(redirect_uri.into_url()?); Ok(self) } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index f69334b8..12f1250e 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -67,7 +67,7 @@ impl ClientCredentialsAuthorizationUrl { let mut url = Url::parse( serializer .get(OAuthParameter::AuthorizationUrl) - .ok_or(AuthorizationFailure::err( + .ok_or(AuthorizationFailure::required( OAuthParameter::AuthorizationUrl.alias(), ))? .as_str(), diff --git a/graph-oauth/src/identity/credentials/crypto.rs b/graph-oauth/src/identity/credentials/crypto.rs index 29143fd0..7848911e 100644 --- a/graph-oauth/src/identity/credentials/crypto.rs +++ b/graph-oauth/src/identity/credentials/crypto.rs @@ -1,16 +1,26 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; +use graph_error::{AuthorizationFailure, AuthorizationResult}; use ring::rand::SecureRandom; pub struct Crypto; impl Crypto { - pub fn sha256_secure_string() -> anyhow::Result<(String, String)> { + /// Generate a secure 43-octet URL safe string for use as a nonce + /// parameter or in the proof key for code exchange (PKCE) flow. + /// + /// Internally this method uses the Rust ring cyrpto library to + /// generate a secure random 32-octet sequence that is base64 URL + /// encoded (no padding). This sequence is hashed using SHA256 and + /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. + /// + /// For more info on PKCE and entropy see: <https://tools.ietf.org/html/rfc7519#section-7.2> + pub fn sha256_secure_string() -> AuthorizationResult<(String, String)> { let mut buf = [0; 32]; let rng = ring::rand::SystemRandom::new(); rng.fill(&mut buf) - .map_err(|_| anyhow::Error::msg("ring::error::Unspecified"))?; + .map_err(|_| AuthorizationFailure::unknown("ring::error::Unspecified"))?; // Known as code_verifier in proof key for code exchange let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index ae0b37cc..d8ed68ce 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -3,11 +3,11 @@ use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, }; use crate::oauth::DeviceCode; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; use url::Url; -static DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; +const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; /// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, /// or a printer. To enable this flow, the device has the user visit a webpage in a browser on @@ -69,30 +69,21 @@ impl AuthorizationSerializer for DeviceCodeCredential { .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::msg_err("access_token_url", "Internal Error"), - )?; + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_internal_err("access_token_url"))?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { - let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), - )?; + let uri = self + .serializer + .get(OAuthParameter::RefreshTokenUrl) + .ok_or(AF::msg_internal_err("refresh_token_url"))?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.device_code.is_some() && self.refresh_token.is_some() { - return AuthorizationFailure::msg_result( - format!( - "{} or {}", - OAuthParameter::DeviceCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - "Device code and refresh token should not be set at the same time - Internal Error", - ); - } - if self.client_id.trim().is_empty() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } @@ -105,7 +96,7 @@ impl AuthorizationSerializer for DeviceCodeCredential { if refresh_token.trim().is_empty() { return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - "Either device code or refresh token is required - found empty refresh token", + "Refresh token string is empty - Either device code or refresh token is required", ); } diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs index bbe4260a..422d4c5a 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs @@ -434,10 +434,11 @@ mod test { fn response_type_join_string() { let authorizer = ImplicitCredential::builder() .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") - .with_response_type(ResponseType::FromString(vec![ - "id_token".to_owned(), - "token".to_owned(), - ])) + .with_response_type(ResponseType::StringSet( + vec!["id_token".to_owned(), "token".to_owned()] + .into_iter() + .collect(), + )) .with_redirect_uri("https::/localhost:8080/myapp") .with_scope(["User.Read"]) .with_nonce("678910") diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index ae2c4b73..9c2d2863 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -23,18 +23,25 @@ pub struct OpenIdAuthorizationUrl { /// by your app. It must exactly match one of the redirect URIs you registered in the portal, /// except that it must be URL-encoded. If not present, the endpoint will pick one registered /// redirect_uri at random to send the user back to. - pub(crate) redirect_uri: Option<String>, + pub(crate) redirect_uri: Option<Url>, /// Required /// Must include code for OpenID Connect sign-in. pub(crate) response_type: BTreeSet<ResponseType>, /// Optional /// Specifies how the identity platform should return the requested token to your app. /// + /// Specifies the method that should be used to send the resulting authorization code back + /// to your app. + /// + /// Can be form_post or fragment. + /// + /// For web applications, Microsoft recommends using response_mode=form_post, + /// to ensure the most secure transfer of tokens to your application. + /// + /// Open Id does not support the query response mode. + /// /// Supported values: /// - /// - query: Default when requesting an access token. Provides the code as a query string - /// parameter on your redirect URI. The query parameter isn't supported when requesting an - /// ID token by using the implicit flow. /// - fragment: Default when requesting an ID token by using the implicit flow. /// Also supported if requesting only a code. /// - form_post: Executes a POST containing the code to your redirect URI. @@ -58,7 +65,7 @@ pub struct OpenIdAuthorizationUrl { /// A space-separated list of scopes. For OpenID Connect, it must include the scope openid, /// which translates to the Sign you in permission in the consent UI. You might also include /// other scopes in this request for requesting consent. - pub(crate) scope: Vec<String>, + pub(crate) scope: BTreeSet<String>, /// Optional /// Indicates the type of user interaction that is required. The only valid values at /// this time are login, none, consent, and select_account. @@ -89,33 +96,51 @@ pub struct OpenIdAuthorizationUrl { /// Optional /// You can use this parameter to pre-fill the username and email address field of the /// sign-in page for the user, if you know the username ahead of time. Often, apps use - /// this parameter during reauthentication, after already extracting the login_hint + /// this parameter during re-authentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, pub(crate) authority: Authority, + response_types_supported: Vec<String>, } impl OpenIdAuthorizationUrl { - pub fn new<T: AsRef<str>>(client_id: T) -> anyhow::Result<OpenIdAuthorizationUrl> { - let mut response_type = BTreeSet::new(); - response_type.insert(ResponseType::Code); - - Ok(OpenIdAuthorizationUrl { + pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + client_id: T, + redirect_uri: Option<T>, + scope: I, + ) -> AuthorizationResult<OpenIdAuthorizationUrl> { + let mut scope_set = BTreeSet::new(); + scope_set.insert("openid".to_owned()); + scope_set.extend(scope.into_iter().map(|s| s.to_string())); + + let mut open_id_url = OpenIdAuthorizationUrl { client_id: client_id.as_ref().to_owned(), redirect_uri: None, - response_type, + response_type: BTreeSet::new(), response_mode: None, nonce: Crypto::sha256_secure_string()?.1, state: None, - scope: vec!["openid".to_owned()], + scope: scope_set, prompt: BTreeSet::new(), domain_hint: None, login_hint: None, authority: Authority::default(), - }) + response_types_supported: vec![ + "code".into(), + "id_token".into(), + "code id_token".into(), + "id_token token".into(), + ], + }; + + if let Some(redirect_uri) = redirect_uri.as_ref() { + open_id_url.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); + } + + Ok(open_id_url) } - pub fn builder() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + pub fn builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { OpenIdAuthorizationUrlBuilder::new() } @@ -173,7 +198,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { if self.nonce.is_empty() { return AuthorizationFailure::msg_result( "nonce", - "nonce is empty - nonce can be automatically generated if not updated by the caller" + "nonce is empty - nonce is automatically generated if not updated by the caller", ); } @@ -183,6 +208,27 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { .nonce(self.nonce.as_str()) .authority(azure_authority_host, &self.authority); + if self.response_type.is_empty() { + serializer.response_type("code"); + } else { + let response_types = self.response_type.as_query(); + if !self.response_types_supported.contains(&response_types) { + return AuthorizationFailure::msg_result( + "response_type", + format!( + "response_type is not supported - supported response types are: {}", + self.response_types_supported + .iter() + .map(|s| format!("`{}`", s)) + .collect::<Vec<String>>() + .join(", ") + ), + ); + } + + serializer.response_types(self.response_type.iter()); + } + if let Some(redirect_uri) = self.redirect_uri.as_ref() { serializer.redirect_uri(redirect_uri.as_ref()); } @@ -236,15 +282,49 @@ pub struct OpenIdAuthorizationUrlBuilder { } impl OpenIdAuthorizationUrlBuilder { - pub(crate) fn new() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + pub(crate) fn new() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { + let mut scope = BTreeSet::new(); + scope.insert("openid".to_owned()); + Ok(OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl::new(String::with_capacity(32))?, + auth_url_parameters: OpenIdAuthorizationUrl { + client_id: String::with_capacity(32), + redirect_uri: None, + response_type: BTreeSet::new(), + response_mode: None, + nonce: Crypto::sha256_secure_string()?.1, + state: None, + scope, + prompt: Default::default(), + domain_hint: None, + login_hint: None, + authority: Default::default(), + response_types_supported: vec![ + "code".into(), + "id_token".into(), + "code id_token".into(), + "id_token token".into(), + ], + }, }) } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.auth_url_parameters.redirect_uri = Some(redirect_uri.as_ref().to_owned()); - self + pub fn new_with_secure_nonce(&mut self) -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + Ok(OpenIdAuthorizationUrlBuilder { + auth_url_parameters: OpenIdAuthorizationUrl::new( + String::with_capacity(32), + None, + BTreeSet::<String>::new(), + )?, + }) + } + + pub fn with_redirect_uri<T: AsRef<str>>( + &mut self, + redirect_uri: T, + ) -> anyhow::Result<&mut Self> { + self.auth_url_parameters.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); + Ok(self) } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { @@ -263,15 +343,21 @@ impl OpenIdAuthorizationUrlBuilder { self } - /// Default is code. Must include code for the authorization code flow. + /// Default is code. + /// Must include code for the open id connect flow. /// Can also include id_token or token if using the hybrid flow. + /// + /// Supported response types are: + /// + /// code + /// id_token + /// code id_token + /// id_token token pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( &mut self, response_type: I, ) -> &mut Self { - self.auth_url_parameters - .response_type - .extend(response_type.into_iter()); + self.auth_url_parameters.response_type = BTreeSet::from_iter(response_type.into_iter()); self } @@ -279,15 +365,14 @@ impl OpenIdAuthorizationUrlBuilder { /// /// Supported values: /// - /// - **query**: Default when requesting an access token. Provides the code as a query string - /// parameter on your redirect URI. The query parameter is not supported when requesting an - /// ID token by using the implicit flow. /// - **fragment**: Default when requesting an ID token by using the implicit flow. /// Also supported if requesting only a code. /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.auth_url_parameters.response_mode = Some(response_mode); + if !response_mode.eq(&ResponseMode::Query) { + self.auth_url_parameters.response_mode = Some(response_mode); + } self } @@ -302,7 +387,11 @@ impl OpenIdAuthorizationUrlBuilder { /// authorization code grant. If you are unsure or unclear how the nonce works then it is /// recommended to stay with the generated nonce as it is cryptographically secure. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.auth_url_parameters.nonce = nonce.as_ref().to_owned(); + if self.auth_url_parameters.nonce.is_empty() { + self.auth_url_parameters.nonce.push_str(nonce.as_ref()); + } else { + self.auth_url_parameters.nonce = nonce.as_ref().to_owned(); + } self } @@ -328,18 +417,32 @@ impl OpenIdAuthorizationUrlBuilder { self } - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self { - self.auth_url_parameters.scope = scopes.into_iter().map(|s| s.to_string()).collect(); + /// Takes an iterator of scopes to use in the request. + /// Replaces current scopes if any were added previously. + /// To extend scopes use [OpenIdAuthorizationUrlBuilder::extend_scope]. + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.auth_url_parameters.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + /// Extend the current list of scopes. + pub fn extend_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.auth_url_parameters + .scope + .extend(scope.into_iter().map(|s| s.to_string())); self } - /// Automatically adds profile, email, id_token, and offline_access to the scope parameter. + /// Automatically adds profile, email, and offline_access to the scope parameter. /// The openid scope is already included when using [OpenIdCredential] pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; - self.with_response_mode(ResponseMode::FormPost); self.with_response_type(vec![ResponseType::Code, ResponseType::IdToken]); - self.with_scope(vec!["profile", "email", "id_token", "offline_access"]); + self.auth_url_parameters.scope.extend( + vec!["profile", "email", "offline_access"] + .into_iter() + .map(|s| s.to_string()), + ); Ok(self) } @@ -386,3 +489,20 @@ impl OpenIdAuthorizationUrlBuilder { self.auth_url_parameters.url() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn unsupported_response_type() { + let _ = OpenIdAuthorizationUrl::builder() + .unwrap() + .with_response_type([ResponseType::Code, ResponseType::Token]) + .with_client_id("client_id") + .with_scope(["scope"]) + .url() + .unwrap(); + } +} diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 07f4e86a..c706767a 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -91,7 +91,7 @@ impl OpenIdCredential { OpenIdCredentialBuilder::new() } - pub fn authorization_url_builder() -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { + pub fn authorization_url_builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { OpenIdAuthorizationUrlBuilder::new() } } @@ -112,13 +112,13 @@ impl AuthorizationSerializer for OpenIdCredential { let uri = self .serializer .get(OAuthParameter::AccessTokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + .ok_or(AF::msg_internal_err("access_token_url"))?; Url::parse(uri.as_str()).map_err(AF::from) } else { let uri = self .serializer .get(OAuthParameter::RefreshTokenUrl) - .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; + .ok_or(AF::msg_internal_err("refresh_token_url"))?; Url::parse(uri.as_str()).map_err(AF::from) } } @@ -270,7 +270,7 @@ impl From<OpenIdAuthorizationUrl> for OpenIdCredentialBuilder { fn from(value: OpenIdAuthorizationUrl) -> Self { let mut builder = OpenIdCredentialBuilder::new(); if let Some(redirect_uri) = value.redirect_uri.as_ref() { - let _ = builder.with_redirect_uri(redirect_uri); + let _ = builder.with_redirect_uri(redirect_uri.clone()); } builder .with_scope(value.scope) diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index a2ccd145..ee15f7ec 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -1,10 +1,24 @@ +use crate::identity::AsQuery; +use std::collections::BTreeSet; + #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ResponseType { #[default] Code, Token, IdToken, - FromString(Vec<String>), + StringSet(BTreeSet<String>), +} + +impl ResponseType { + pub fn try_from_set(response_types: &BTreeSet<ResponseType>) -> String { + dbg!(response_types); + + info!("{:#?}", &response_types); + let response_type_list: Vec<String> = + response_types.iter().map(|rt| rt.to_string()).collect(); + response_type_list.join(" ") + } } impl ToString for ResponseType { @@ -13,13 +27,7 @@ impl ToString for ResponseType { ResponseType::Code => "code".to_owned(), ResponseType::Token => "token".to_owned(), ResponseType::IdToken => "id_token".to_owned(), - ResponseType::FromString(response_type_vec) => { - let response_types: Vec<String> = response_type_vec - .iter() - .map(|s| s.trim().to_owned()) - .collect(); - response_types.join(" ") - } + ResponseType::StringSet(response_type_vec) => response_type_vec.iter().as_query(), } } } @@ -32,3 +40,28 @@ impl IntoIterator for ResponseType { vec![self].into_iter() } } + +impl<A: ToString> std::iter::FromIterator<A> for ResponseType { + fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self { + let vec: BTreeSet<String> = iter.into_iter().map(|v| v.to_string()).collect(); + ResponseType::StringSet(vec) + } +} + +impl AsQuery for Vec<ResponseType> { + fn as_query(&self) -> String { + self.iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + } +} + +impl AsQuery for BTreeSet<ResponseType> { + fn as_query(&self) -> String { + self.iter() + .map(|s| s.to_string()) + .collect::<Vec<String>>() + .join(" ") + } +} diff --git a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs index ac090d6d..e882393d 100644 --- a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::oauth::ResponseType; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use url::form_urlencoded::Serializer; use url::Url; @@ -71,7 +71,7 @@ impl TokenFlowAuthorizationUrl { url.set_query(Some(encoder.finish().as_str())); Ok(url) } else { - AuthorizationFailure::msg_result("authorization_url", "Internal Error") + AF::msg_internal_result("authorization_url") } } } From 7ee76e2388111a3374013850a94981e7baa23039 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 1 Jun 2023 01:20:32 -0400 Subject: [PATCH 023/118] Updates to identity credential builder and clean up --- .../oauth/auth_code_grant_refresh_token.rs | 2 +- examples/oauth/implicit_grant.rs | 5 +- examples/oauth/main.rs | 2 +- examples/oauth/open_id_connect.rs | 12 +-- graph-oauth/src/access_token.rs | 24 +++--- graph-oauth/src/auth.rs | 74 +---------------- .../auth_code_authorization_url.rs | 79 +++++++++---------- ...thorization_code_certificate_credential.rs | 2 +- .../authorization_code_credential.rs | 3 +- .../client_certificate_credential.rs | 18 ++--- .../credentials/client_secret_credential.rs | 2 +- .../confidential_client_application.rs | 2 +- .../credentials/credential_builder.rs | 35 +++----- .../credentials/device_code_credential.rs | 45 +++-------- .../credentials/environment_credential.rs | 76 ++++++------------ ...rization_url.rs => implicit_credential.rs} | 2 +- .../code_flow_authorization_url.rs | 10 +-- .../{ => legacy}/code_flow_credential.rs | 0 .../src/identity/credentials/legacy/mod.rs | 7 ++ .../token_flow_authorization_url.rs | 10 +-- graph-oauth/src/identity/credentials/mod.rs | 12 +-- .../credentials/open_id_credential.rs | 2 +- .../resource_owner_password_credential.rs | 61 +++++++++++--- .../identity/credentials/x509_certificate.rs | 56 +++++-------- graph-oauth/src/identity/form_credential.rs | 7 -- graph-oauth/src/identity/mod.rs | 1 - test-tools/src/oauth_request.rs | 2 +- 27 files changed, 214 insertions(+), 337 deletions(-) rename graph-oauth/src/identity/credentials/{implicit_credential_authorization_url.rs => implicit_credential.rs} (99%) rename graph-oauth/src/identity/credentials/{ => legacy}/code_flow_authorization_url.rs (92%) rename graph-oauth/src/identity/credentials/{ => legacy}/code_flow_credential.rs (100%) create mode 100644 graph-oauth/src/identity/credentials/legacy/mod.rs rename graph-oauth/src/identity/credentials/{ => legacy}/token_flow_authorization_url.rs (90%) delete mode 100644 graph-oauth/src/identity/form_credential.rs diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant_refresh_token.rs index e7855fd4..9d3ffd5f 100644 --- a/examples/oauth/auth_code_grant_refresh_token.rs +++ b/examples/oauth/auth_code_grant_refresh_token.rs @@ -1,6 +1,6 @@ use graph_oauth::identity::AuthorizationCodeCredentialBuilder; use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, CredentialBuilder, TokenRequest, + AuthorizationCodeCredential, ConfidentialClientApplication, TokenRequest, }; // Use a refresh token to get a new access token. diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index 31e16c9f..c376850c 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeSet; // The following example shows authenticating an application to use the OneDrive REST API // for a native client. Native clients typically use the implicit OAuth flow. This requires // using the browser to log in. To get an access token, set the response type to 'token' @@ -51,9 +52,9 @@ fn multi_response_types() { // Or let _ = ImplicitCredential::builder() - .with_response_type(ResponseType::StringSet(vec![ + .with_response_type(ResponseType::StringSet(BTreeSet::from_iter(vec![ "token".to_string(), "id_token".to_string(), - ])) + ]))) .build(); } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 3877220a..5077118d 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -31,7 +31,7 @@ mod signing_keys; use graph_rs_sdk::oauth::{ AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - CredentialBuilder, DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenRequest, }; diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index 81ac7809..b154ac04 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,4 +1,4 @@ -use graph_oauth::identity::{CredentialBuilder, ResponseType, TokenRequest}; +use graph_oauth::identity::{ResponseType, TokenRequest}; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; use url::Url; @@ -18,12 +18,9 @@ use url::Url; /// OAuth-enabled applications by using a security token called an ID token. use warp::Filter; -// "client_id": "e0951f73-cafa-455f-9365-50dfd22f56b6", -// "client_secret": "rUWHfYygz~IZH~7I~2.w1-Sedf~T16g8OR", - // The client id and client secret must be changed before running this example. -static CLIENT_ID: &str = "e0951f73-cafa-455f-9365-50dfd22f56b6"; -static CLIENT_SECRET: &str = "rUWHfYygz~IZH~7I~2.w1-Sedf~T16g8OR"; +static CLIENT_ID: &str = ""; +static CLIENT_SECRET: &str = ""; fn open_id_credential( authorization_code: &str, @@ -40,8 +37,7 @@ fn open_id_credential( } fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { - Ok(OpenIdCredential::authorization_url_builder() - .new_with_secure_nonce()? + Ok(OpenIdCredential::authorization_url_builder()? .with_client_id(client_id) .with_default_scope()? .extend_scope(vec!["Files.Read"]) diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 33e9b5d7..9e741fc5 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -409,19 +409,19 @@ impl<'de> Deserialize<'de> for AccessToken { where D: Deserializer<'de>, { - let inner_access_token: PhantomAccessToken = Deserialize::deserialize(deserializer)?; + let phantom_access_token: PhantomAccessToken = Deserialize::deserialize(deserializer)?; Ok(AccessToken { - access_token: inner_access_token.access_token, - token_type: inner_access_token.token_type, - expires_in: inner_access_token.expires_in, - ext_expires_in: inner_access_token.ext_expires_in, - scope: inner_access_token.scope, - refresh_token: inner_access_token.refresh_token, - user_id: inner_access_token.user_id, - id_token: inner_access_token.id_token, - state: inner_access_token.state, - timestamp: Some(Utc::now() + Duration::seconds(inner_access_token.expires_in)), - additional_fields: inner_access_token.additional_fields, + access_token: phantom_access_token.access_token, + token_type: phantom_access_token.token_type, + expires_in: phantom_access_token.expires_in, + ext_expires_in: phantom_access_token.ext_expires_in, + scope: phantom_access_token.scope, + refresh_token: phantom_access_token.refresh_token, + user_id: phantom_access_token.user_id, + id_token: phantom_access_token.id_token, + state: phantom_access_token.state, + timestamp: Some(Utc::now() + Duration::seconds(phantom_access_token.expires_in)), + additional_fields: phantom_access_token.additional_fields, log_pii: false, }) } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 50245cc6..73a03055 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,13 +1,12 @@ use crate::access_token::AccessToken; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; -use crate::identity::form_credential::ParameterIs; use crate::identity::{AsQuery, Authority, AzureAuthorityHost, Prompt}; use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use graph_error::{AF, AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; use ring::rand::SecureRandom; use std::collections::btree_map::BTreeMap; use std::collections::{BTreeSet, HashMap}; @@ -152,7 +151,6 @@ pub struct OAuthSerializer { access_token: Option<AccessToken>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, - parameters: HashMap<ParameterIs, String>, } impl OAuthSerializer { @@ -169,7 +167,6 @@ impl OAuthSerializer { access_token: None, scopes: BTreeSet::new(), credentials: BTreeMap::new(), - parameters: HashMap::with_capacity(10), } } @@ -224,7 +221,8 @@ impl OAuthSerializer { | OAuthParameter::AccessTokenUrl | OAuthParameter::AuthorizationUrl | OAuthParameter::LogoutURL => { - Url::parse(v.as_ref()).unwrap(); + Url::parse(v.as_ref()).map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) + .unwrap(); } _ => {} } @@ -1101,39 +1099,6 @@ impl OAuthSerializer { Ok(()) } - pub fn url_query_encode( - &mut self, - pairs: Vec<ParameterIs>, - encoder: &mut Serializer<String>, - ) -> AuthorizationResult<()> { - for form_credential in pairs.iter() { - match form_credential { - ParameterIs::Required(oac) => { - if oac.alias().eq("scope") { - if self.scopes.is_empty() { - return AuthorizationFailure::result::<()>(oac.alias()); - } else { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); - } - } else { - let value = self.get(*oac).ok_or(AuthorizationFailure::required(oac))?; - - encoder.append_pair(oac.alias(), value.as_str()); - } - } - ParameterIs::Optional(oac) => { - if oac.alias().eq("scope") && !self.scopes.is_empty() { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); - } else if let Some(val) = self.get(*oac) { - encoder.append_pair(oac.alias(), val.as_str()); - } - } - } - } - - Ok(()) - } - pub fn params(&mut self, pairs: Vec<OAuthParameter>) -> GraphResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for oac in pairs.iter() { @@ -1171,39 +1136,6 @@ impl OAuthSerializer { Ok(required_map) } - pub fn authorization_form( - &mut self, - form_credentials: Vec<ParameterIs>, - ) -> AuthorizationResult<HashMap<String, String>> { - let mut map: HashMap<String, String> = HashMap::new(); - - for form_credential in form_credentials.iter() { - match form_credential { - ParameterIs::Required(oac) => { - let val = self - .get(*oac) - .ok_or(AuthorizationFailure::required(oac.alias()))?; - if val.trim().is_empty() { - return AuthorizationFailure::msg_result(oac, "Value cannot be empty"); - } else { - map.insert(oac.to_string(), val); - } - } - ParameterIs::Optional(oac) => { - if oac.eq(&OAuthParameter::Scope) && !self.scopes.is_empty() { - map.insert("scope".into(), self.join_scopes(" ")); - } else if let Some(val) = self.get(*oac) { - if !val.trim().is_empty() { - map.insert(oac.to_string(), val); - } - } - } - } - } - - Ok(map) - } - pub fn encode_uri( &mut self, grant: GrantType, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 7cf7c0f0..19a55a2c 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -135,9 +135,10 @@ impl AuthCodeAuthorizationUrl { */ let url = Url::parse(&url_string)?; - let query = url.query().ok_or(AF::msg_err( - "query", - &format!("Url returned on redirect is missing query parameters, url: {url}"), + let query = url.query() + .or(url.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect, url: {url}"), ))?; let response_query: AuthQueryResponse = serde_urlencoded::from_str(query)?; @@ -197,19 +198,19 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::result("redirect_uri"); + return AF::result("redirect_uri"); } if self.client_id.trim().is_empty() { - return AuthorizationFailure::result("client_id"); + return AF::result("client_id"); } if self.scope.is_empty() { - return AuthorizationFailure::result("scope"); + return AF::result("scope"); } if self.scope.contains(&String::from("openid")) { - return AuthorizationFailure::msg_result( + return AF::msg_result( "openid", "Scope openid is not valid for authorization code - instead use OpenIdCredential", ); @@ -239,16 +240,10 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { // Set response_mode if self.response_type.contains(&ResponseType::IdToken) { - if let Some(response_mode) = self.response_mode.as_ref() { - // id_token requires fragment or form_post. The Microsoft identity - // platform recommends form_post but fragment is default. - if response_mode.eq(&ResponseMode::Query) { - serializer.response_mode(ResponseMode::Fragment.as_ref()); - } else { - serializer.response_mode(response_mode.as_ref()); - } - } else { + if self.response_mode.is_none() || self.response_mode.eq(&Some(ResponseMode::Query)) { serializer.response_mode(ResponseMode::Fragment.as_ref()); + } else if let Some(response_mode) = self.response_mode.as_ref() { + serializer.response_mode(response_mode.as_ref()); } } else if let Some(response_mode) = self.response_mode.as_ref() { serializer.response_mode(response_mode.as_ref()); @@ -315,7 +310,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { #[derive(Clone)] pub struct AuthCodeAuthorizationUrlBuilder { - auth_url_parameters: AuthCodeAuthorizationUrl, + authorization_url: AuthCodeAuthorizationUrl, } impl Default for AuthCodeAuthorizationUrlBuilder { @@ -329,8 +324,8 @@ impl AuthCodeAuthorizationUrlBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlBuilder { - auth_url_parameters: AuthCodeAuthorizationUrl { - client_id: String::new(), + authorization_url: AuthCodeAuthorizationUrl { + client_id: String::with_capacity(32), redirect_uri: String::new(), authority: Authority::default(), response_mode: None, @@ -348,23 +343,23 @@ impl AuthCodeAuthorizationUrlBuilder { } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.auth_url_parameters.redirect_uri = redirect_uri.as_ref().to_owned(); + self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); self } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.auth_url_parameters.client_id = client_id.as_ref().to_owned(); + self.authorization_url.client_id = client_id.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.auth_url_parameters.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.authorization_url.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.auth_url_parameters.authority = authority.into(); + self.authorization_url.authority = authority.into(); self } @@ -374,7 +369,7 @@ impl AuthCodeAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.auth_url_parameters + self.authorization_url .response_type .extend(response_type.into_iter()); self @@ -392,7 +387,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.auth_url_parameters.response_mode = Some(response_mode); + self.authorization_url.response_mode = Some(response_mode); self } @@ -401,7 +396,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.auth_url_parameters.nonce = Some(nonce.as_ref().to_owned()); + self.authorization_url.nonce = Some(nonce.as_ref().to_owned()); self } @@ -418,12 +413,12 @@ impl AuthCodeAuthorizationUrlBuilder { /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.nonce = Some(Crypto::sha256_secure_string()?.1); + self.authorization_url.nonce = Some(Crypto::sha256_secure_string()?.1); Ok(self) } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.auth_url_parameters.state = Some(state.as_ref().to_owned()); + self.authorization_url.state = Some(state.as_ref().to_owned()); self } @@ -433,16 +428,16 @@ impl AuthCodeAuthorizationUrlBuilder { /// and generates a secure nonce value. /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.auth_url_parameters.scope.extend( + self.authorization_url.scope.extend( scope .into_iter() .map(|s| s.to_string()) .map(|s| s.trim().to_owned()), ); - if self.auth_url_parameters.nonce.is_none() + if self.authorization_url.nonce.is_none() && self - .auth_url_parameters + .authorization_url .scope .contains(&String::from("id_token")) { @@ -456,7 +451,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// If you need a refresh token then include `offline_access` as a scope. /// The `offline_access` scope is not included here. pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters + self.authorization_url .scope .extend(vec!["profile".to_owned(), "email".to_owned()]); Ok(self) @@ -465,7 +460,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// Adds the `offline_access` scope parameter which tells the authorization server /// to include a refresh token in the redirect uri query. pub fn with_refresh_token_scope(&mut self) -> &mut Self { - self.auth_url_parameters + self.authorization_url .scope .extend(vec!["offline_access".to_owned()]); self @@ -482,10 +477,10 @@ impl AuthCodeAuthorizationUrlBuilder { /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; - self.auth_url_parameters + self.authorization_url .response_type .extend(ResponseType::IdToken); - self.auth_url_parameters + self.authorization_url .scope .extend(vec!["id_token".to_owned()]); Ok(self) @@ -502,24 +497,24 @@ impl AuthCodeAuthorizationUrlBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.auth_url_parameters.prompt = Some(prompt); + self.authorization_url.prompt = Some(prompt); self } pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.auth_url_parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.authorization_url.domain_hint = Some(domain_hint.as_ref().to_owned()); self } pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.auth_url_parameters.login_hint = Some(login_hint.as_ref().to_owned()); + self.authorization_url.login_hint = Some(login_hint.as_ref().to_owned()); self } /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). /// Required if code_challenge_method is included. pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { - self.auth_url_parameters.code_challenge = Some(code_challenge.as_ref().to_owned()); + self.authorization_url.code_challenge = Some(code_challenge.as_ref().to_owned()); self } @@ -532,7 +527,7 @@ impl AuthCodeAuthorizationUrlBuilder { &mut self, code_challenge_method: T, ) -> &mut Self { - self.auth_url_parameters.code_challenge_method = + self.authorization_url.code_challenge_method = Some(code_challenge_method.as_ref().to_owned()); self } @@ -550,11 +545,11 @@ impl AuthCodeAuthorizationUrlBuilder { } pub fn build(&self) -> AuthCodeAuthorizationUrl { - self.auth_url_parameters.clone() + self.authorization_url.clone() } pub fn url(&self) -> AuthorizationResult<Url> { - self.auth_url_parameters.url() + self.authorization_url.url() } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 28113381..853d993f 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, - AzureAuthorityHost, CredentialBuilder, TokenCredential, TokenCredentialOptions, TokenRequest, + AzureAuthorityHost, TokenCredential, TokenCredentialOptions, TokenRequest, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 342b0462..c2c37d6c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,8 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, - CredentialBuilder, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, - TokenRequest, + ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; use crate::oauth::AuthCodeAuthorizationUrlBuilder; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 46037e68..40d2ccfd 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,10 +1,10 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, TokenCredential, + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AF, AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -81,14 +81,14 @@ impl AuthorizationSerializer for ClientCertificateCredential { if self.refresh_token.is_none() { let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::msg_err("access_token_url", "Internal Error"), + AF::msg_err("access_token_url", "Internal Error"), )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + Url::parse(uri.as_str()).map_err(AF::from) } else { let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), + AF::msg_err("refresh_token_url", "Internal Error"), )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + Url::parse(uri.as_str()).map_err(AF::from) } } @@ -182,12 +182,12 @@ impl ClientCertificateCredentialBuilder { #[cfg(feature = "openssl")] pub fn with_certificate( &mut self, - certificate_assertion: &X509Certificate, + certificate: &X509Certificate, ) -> anyhow::Result<&mut Self> { if let Some(tenant_id) = self.credential.authority.tenant_id() { - self.with_client_assertion(certificate_assertion.sign(Some(tenant_id.clone()))?); + self.with_client_assertion(certificate.sign(Some(tenant_id.clone()))?); } else { - self.with_client_assertion(certificate_assertion.sign(None)?); + self.with_client_assertion(certificate.sign(None)?); } Ok(self) } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 0aa15b24..d67c4619 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ Authority, AuthorizationSerializer, AzureAuthorityHost, - ClientCredentialsAuthorizationUrlBuilder, CredentialBuilder, TokenCredential, TokenRequest, + ClientCredentialsAuthorizationUrlBuilder, TokenCredential, TokenRequest, }; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 0d4b0af8..d8bdea32 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -227,7 +227,7 @@ impl From<OpenIdCredential> for ConfidentialClientApplication { #[cfg(test)] mod test { use super::*; - use crate::identity::{Authority, AzureAuthorityHost, CredentialBuilder}; + use crate::identity::{Authority, AzureAuthorityHost}; #[test] fn confidential_client_new() { diff --git a/graph-oauth/src/identity/credentials/credential_builder.rs b/graph-oauth/src/identity/credentials/credential_builder.rs index 7a2a9e4e..3290ae44 100644 --- a/graph-oauth/src/identity/credentials/credential_builder.rs +++ b/graph-oauth/src/identity/credentials/credential_builder.rs @@ -1,22 +1,7 @@ -use crate::identity::{Authority, TokenCredentialOptions}; - -pub trait CredentialBuilder { - type Credential; - - fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self; - fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self; - fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self; - fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scopes: I) -> &mut Self; - fn with_token_credential_options(&mut self, options: TokenCredentialOptions) -> &mut Self; - fn build(&self) -> Self::Credential; -} - macro_rules! credential_builder_impl { ($name:ident, $credential:ty) => { - impl CredentialBuilder for $name { - type Credential = $credential; - - fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + impl $name { + pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { if self.credential.client_id.is_empty() { self.credential.client_id.push_str(client_id.as_ref()); } else { @@ -26,17 +11,17 @@ macro_rules! credential_builder_impl { } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + pub fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { + self.credential.authority = crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); self } - fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + pub fn with_authority<T: Into<crate::identity::Authority>>(&mut self, authority: T) -> &mut Self { self.credential.authority = authority.into(); self } - fn with_scope<T: ToString, I: IntoIterator<Item = T>>( + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( &mut self, scope: I, ) -> &mut Self { @@ -44,17 +29,17 @@ macro_rules! credential_builder_impl { self } - fn with_token_credential_options( + pub fn with_token_credential_options( &mut self, - options: TokenCredentialOptions, + options: crate::identity::TokenCredentialOptions, ) -> &mut Self { self.credential.token_credential_options = options; self } - fn build(&self) -> $credential { + pub fn build(&self) -> $credential { self.credential.clone() } } - }; + } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index d8ed68ce..2e039a5c 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -9,6 +9,11 @@ use url::Url; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; +credential_builder_impl!( + DeviceCodeCredentialBuilder, + DeviceCodeCredential +); + /// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, /// or a printer. To enable this flow, the device has the user visit a webpage in a browser on /// another device to sign in. Once the user signs in, the device is able to get access tokens @@ -58,6 +63,12 @@ impl DeviceCodeCredential { } } + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.device_code = None; + self.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + pub fn builder() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new() } @@ -157,7 +168,7 @@ impl DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { credential: DeviceCodeCredential { refresh_token: None, - client_id: String::new(), + client_id: String::with_capacity(32), device_code: None, scope: vec![], authority: Default::default(), @@ -167,11 +178,6 @@ impl DeviceCodeCredentialBuilder { } } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self - } - pub fn with_device_code<T: AsRef<str>>(&mut self, device_code: T) -> &mut Self { self.credential.device_code = Some(device_code.as_ref().to_owned()); self.credential.refresh_token = None; @@ -183,33 +189,6 @@ impl DeviceCodeCredentialBuilder { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.credential.authority = authority.into(); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } - - pub fn build(&self) -> DeviceCodeCredential { - self.credential.clone() - } } impl From<&DeviceCode> for DeviceCodeCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 279f9403..5df81bb2 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -36,35 +36,22 @@ impl EnvironmentCredential { fn try_azure_client_secret_compile_time_env() -> Result<ConfidentialClientApplication, VarError> { - let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let tenant_id = option_env!("AZURE_TENANT_ID"); let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_client_secret = option_env!("AZURE_CLIENT_SECRET").ok_or(VarError::NotPresent)?; - - match tenant_id_option { - Some(tenant_id) => Ok(ConfidentialClientApplication::new( - ClientSecretCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_client_secret, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), - None => Ok(ConfidentialClientApplication::new( - ClientSecretCredential::new(azure_client_id, azure_client_secret), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), - } + EnvironmentCredential::client_secret_env(tenant_id.map(|s| s.to_owned()), azure_client_id.to_owned(), azure_client_secret.to_owned()) } fn try_azure_client_secret_runtime_env() -> Result<ConfidentialClientApplication, VarError> { - let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let tenant_id = std::env::var(AZURE_TENANT_ID).ok(); let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_client_secret = std::env::var(AZURE_CLIENT_SECRET)?; + EnvironmentCredential::client_secret_env(tenant_id, azure_client_id, azure_client_secret) + } - if let Ok(tenant_id) = tenant_id_result { - Ok(ConfidentialClientApplication::new( + fn client_secret_env(tenant_id: Option<String>, azure_client_id: String, azure_client_secret: String) -> Result<ConfidentialClientApplication, VarError> { + match tenant_id { + Some(tenant_id) => Ok(ConfidentialClientApplication::new( ClientSecretCredential::new_with_tenant( tenant_id, azure_client_id, @@ -72,53 +59,34 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?) - } else { - Ok(ConfidentialClientApplication::new( + .map_err(|_| VarError::NotPresent)?), + None => Ok(ConfidentialClientApplication::new( ClientSecretCredential::new(azure_client_id, azure_client_secret), Default::default(), ) - .map_err(|_| VarError::NotPresent)?) + .map_err(|_| VarError::NotPresent)?), } } fn try_username_password_compile_time_env() -> Result<PublicClientApplication, VarError> { - let tenant_id_option = option_env!("AZURE_TENANT_ID"); + let tenant_id = option_env!("AZURE_TENANT_ID"); let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_username = option_env!("AZURE_USERNAME").ok_or(VarError::NotPresent)?; let azure_password = option_env!("AZURE_PASSWORD").ok_or(VarError::NotPresent)?; - - match tenant_id_option { - Some(tenant_id) => Ok(PublicClientApplication::new( - ResourceOwnerPasswordCredential::new_with_tenant( - tenant_id, - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), - None => Ok(PublicClientApplication::new( - ResourceOwnerPasswordCredential::new( - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), - } + EnvironmentCredential::username_password_env(tenant_id.map(|s| s.to_owned()), azure_client_id.to_owned(), azure_username.to_owned(), azure_password.to_owned()) } fn try_username_password_runtime_env() -> Result<PublicClientApplication, VarError> { - let tenant_id_result = std::env::var(AZURE_TENANT_ID); + let tenant_id = std::env::var(AZURE_TENANT_ID).ok(); let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_username = std::env::var(AZURE_USERNAME)?; let azure_password = std::env::var(AZURE_PASSWORD)?; + EnvironmentCredential::username_password_env(tenant_id, azure_client_id, azure_username, azure_password) + } - match tenant_id_result { - Ok(tenant_id) => Ok(PublicClientApplication::new( + fn username_password_env(tenant_id: Option<String>, azure_client_id: String, azure_username: String, azure_password: String) -> Result<PublicClientApplication, VarError> { + match tenant_id { + Some(tenant_id) => Ok(PublicClientApplication::new( ResourceOwnerPasswordCredential::new_with_tenant( tenant_id, azure_client_id, @@ -127,8 +95,8 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), - Err(_) => Ok(PublicClientApplication::new( + .map_err(|_| VarError::NotPresent)?), + None => Ok(PublicClientApplication::new( ResourceOwnerPasswordCredential::new( azure_client_id, azure_username, @@ -136,7 +104,7 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), + .map_err(|_| VarError::NotPresent)?), } } } diff --git a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs similarity index 99% rename from graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs rename to graph-oauth/src/identity/credentials/implicit_credential.rs index 422d4c5a..13a9f83e 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AzureAuthorityHost, CredentialBuilder, Crypto, Prompt, ResponseMode, ResponseType, + Authority, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType, }; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; diff --git a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs similarity index 92% rename from graph-oauth/src/identity/credentials/code_flow_authorization_url.rs rename to graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index b2f3cb40..d01d1b38 100644 --- a/graph-oauth/src/identity/credentials/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -92,13 +92,13 @@ impl CodeFlowAuthorizationUrl { #[derive(Clone)] pub struct CodeFlowAuthorizationUrlBuilder { - token_flow_authorization_url: CodeFlowAuthorizationUrl, + authorization_url: CodeFlowAuthorizationUrl, } impl CodeFlowAuthorizationUrlBuilder { fn new() -> CodeFlowAuthorizationUrlBuilder { CodeFlowAuthorizationUrlBuilder { - token_flow_authorization_url: CodeFlowAuthorizationUrl { + authorization_url: CodeFlowAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), response_type: ResponseType::Code, @@ -108,18 +108,18 @@ impl CodeFlowAuthorizationUrlBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.token_flow_authorization_url.client_id = client_id.as_ref().to_owned(); + self.authorization_url.client_id = client_id.as_ref().to_owned(); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.token_flow_authorization_url.scope = + self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.token_flow_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); self } } diff --git a/graph-oauth/src/identity/credentials/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs similarity index 100% rename from graph-oauth/src/identity/credentials/code_flow_credential.rs rename to graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs diff --git a/graph-oauth/src/identity/credentials/legacy/mod.rs b/graph-oauth/src/identity/credentials/legacy/mod.rs new file mode 100644 index 00000000..0926e91a --- /dev/null +++ b/graph-oauth/src/identity/credentials/legacy/mod.rs @@ -0,0 +1,7 @@ +mod code_flow_credential; +mod code_flow_authorization_url; +mod token_flow_authorization_url; + +pub use code_flow_authorization_url::*; +pub use code_flow_credential::*; +pub use token_flow_authorization_url::*; diff --git a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs similarity index 90% rename from graph-oauth/src/identity/credentials/token_flow_authorization_url.rs rename to graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index e882393d..e0afc00d 100644 --- a/graph-oauth/src/identity/credentials/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -78,13 +78,13 @@ impl TokenFlowAuthorizationUrl { #[derive(Clone)] pub struct TokenFlowAuthorizationUrlBuilder { - token_flow_authorization_url: TokenFlowAuthorizationUrl, + authorization_url: TokenFlowAuthorizationUrl, } impl TokenFlowAuthorizationUrlBuilder { fn new() -> TokenFlowAuthorizationUrlBuilder { TokenFlowAuthorizationUrlBuilder { - token_flow_authorization_url: TokenFlowAuthorizationUrl { + authorization_url: TokenFlowAuthorizationUrl { client_id: String::new(), redirect_uri: String::new(), response_type: ResponseType::Token, @@ -94,18 +94,18 @@ impl TokenFlowAuthorizationUrlBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.token_flow_authorization_url.client_id = client_id.as_ref().to_owned(); + self.authorization_url.client_id = client_id.as_ref().to_owned(); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.token_flow_authorization_url.scope = + self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.token_flow_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); + self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); self } } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 13633c82..3a17ea60 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,6 +1,8 @@ #[macro_use] mod credential_builder; +pub mod legacy; + mod as_query; mod auth_code_authorization_url; mod authorization_code_certificate_credential; @@ -9,14 +11,12 @@ mod client_application; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; -mod code_flow_authorization_url; -mod code_flow_credential; mod confidential_client_application; mod crypto; mod device_code_credential; mod display; mod environment_credential; -mod implicit_credential_authorization_url; +mod implicit_credential; mod open_id_authorization_url; mod open_id_credential; mod prompt; @@ -28,7 +28,6 @@ mod response_mode; mod response_type; mod token_credential; mod token_credential_options; -mod token_flow_authorization_url; mod token_request; #[cfg(feature = "openssl")] @@ -42,15 +41,13 @@ pub use client_application::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; -pub use code_flow_authorization_url::*; -pub use code_flow_credential::*; pub use confidential_client_application::*; pub use credential_builder::*; pub(crate) use crypto::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; -pub use implicit_credential_authorization_url::*; +pub use implicit_credential::*; pub use open_id_authorization_url::*; pub use open_id_credential::*; pub use prompt::*; @@ -62,7 +59,6 @@ pub use response_mode::*; pub use response_type::*; pub use token_credential::*; pub use token_credential_options::*; -pub use token_flow_authorization_url::*; pub use token_request::*; #[cfg(feature = "openssl")] diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index c706767a..15393959 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, CredentialBuilder, + Authority, AuthorizationSerializer, AzureAuthorityHost, OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 58fb3a38..2180b9eb 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AF, AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -87,22 +87,22 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { .authority(azure_authority_host, &self.authority); let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::msg_err("access_token_url", "Internal Error"), + AF::msg_err("access_token_url", "Internal Error"), )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); + return AF::result(OAuthParameter::ClientId.alias()); } if self.username.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::Username.alias()); + return AF::result(OAuthParameter::Username.alias()); } if self.password.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::Password.alias()); + return AF::result(OAuthParameter::Password.alias()); } self.serializer @@ -136,7 +136,7 @@ impl ResourceOwnerPasswordCredentialBuilder { fn new() -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { - client_id: String::new(), + client_id: String::with_capacity(32), username: String::new(), password: String::new(), scope: vec![], @@ -148,7 +148,11 @@ impl ResourceOwnerPasswordCredentialBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); + if self.credential.client_id.is_empty() { + self.credential.client_id.push_str(client_id.as_ref()); + } else { + self.credential.client_id = client_id.as_ref().to_owned(); + } self } @@ -178,13 +182,20 @@ impl ResourceOwnerPasswordCredentialBuilder { authority: T, ) -> AuthorizationResult<&mut Self> { let authority = authority.into(); + if vec![Authority::Common, Authority::Consumers, Authority::AzureActiveDirectory].contains(&authority) { + return AF::msg_result( + "tenant_id", + "Authority Azure Active Directory, common, and consumers are not supported authentication contexts for ROPC" + ); + } + if authority.eq(&Authority::Common) || authority.eq(&Authority::AzureActiveDirectory) || authority.eq(&Authority::Consumers) { return AuthorizationFailure::msg_result( "tenant_id", - "Authority Azure Active Directory, common, and consumers are not supported authentication contexts for ROPC" + "ADFS, common, and consumers are not supported authentication contexts for ROPC" ); } @@ -215,3 +226,35 @@ impl Default for ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder::new() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn fail_on_authority_common() { + let _ = ResourceOwnerPasswordCredential::builder() + .with_authority(Authority::Common) + .unwrap() + .build(); + } + + #[test] + #[should_panic] + fn fail_on_authority_adfs() { + let _ = ResourceOwnerPasswordCredential::builder() + .with_authority(Authority::AzureActiveDirectory) + .unwrap() + .build(); + } + + #[test] + #[should_panic] + fn fail_on_authority_consumers() { + let _ = ResourceOwnerPasswordCredential::builder() + .with_authority(Authority::Consumers) + .unwrap() + .build(); + } +} diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index a4cd74a0..885d2d55 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -12,30 +12,26 @@ use std::collections::HashMap; use time::OffsetDateTime; use uuid::Uuid; -pub(crate) trait EncodeCert { - fn encode_cert(cert: &X509) -> anyhow::Result<String> { - Ok(format!( - "\"{}\"", - URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) - )) - } - - fn encode_cert_ref(cert: &X509Ref) -> anyhow::Result<String> { - Ok(format!( - "\"{}\"", - URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) - )) - } +fn encode_cert(cert: &X509) -> anyhow::Result<String> { + Ok(format!( + "\"{}\"", + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + )) +} - fn thumbprint(cert: &X509) -> anyhow::Result<String> { - let digest_bytes = cert - .digest(MessageDigest::sha1()) - .map_err(|err| anyhow!(err.to_string()))?; - Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) - } +fn encode_cert_ref(cert: &X509Ref) -> anyhow::Result<String> { + Ok(format!( + "\"{}\"", + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + )) } -impl EncodeCert for X509Certificate {} +fn thumbprint(cert: &X509) -> anyhow::Result<String> { + let digest_bytes = cert + .digest(MessageDigest::sha1()) + .map_err(|err| anyhow!(err.to_string()))?; + Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) +} /// Computes the client assertion used in certificate credential authorization flows. /// The client assertion is computed from the DER encoding of an X509 certificate and it's private key. @@ -45,7 +41,6 @@ impl EncodeCert for X509Certificate {} /// https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions pub struct X509Certificate { client_id: String, - tenant_id: Option<String>, claims: Option<HashMap<String, String>>, extend_claims: bool, certificate: X509, @@ -59,7 +54,6 @@ impl X509Certificate { pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { Self { client_id: client_id.as_ref().to_owned(), - tenant_id: None, claims: None, extend_claims: true, certificate, @@ -75,7 +69,7 @@ impl X509Certificate { pass: T, certificate: X509, ) -> anyhow::Result<Self> { - let der = X509Certificate::encode_cert(&certificate)?; + let der = encode_cert(&certificate)?; let parsed_pkcs12 = Pkcs12::from_der(&URL_SAFE_NO_PAD.decode(der)?)?.parse2(pass.as_ref())?; @@ -89,7 +83,6 @@ impl X509Certificate { Ok(Self { client_id: client_id.as_ref().to_owned(), - tenant_id: None, claims: None, extend_claims: true, certificate, @@ -100,10 +93,6 @@ impl X509Certificate { }) } - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) { - self.tenant_id = Some(tenant.as_ref().to_owned()); - } - /// Provide your own set of claims in the payload of the JWT. /// /// Replace the claims that would be generated for the client assertion. @@ -180,12 +169,12 @@ impl X509Certificate { "No certificate found after parsing Pkcs12 using pass" ))?; - let sig = X509Certificate::encode_cert(certificate)?; + let sig = encode_cert(certificate)?; if let Some(stack) = parsed_pkcs12.ca.as_ref() { let chain = stack .into_iter() - .map(X509Certificate::encode_cert_ref) + .map(encode_cert_ref) .collect::<anyhow::Result<Vec<String>>>() .map_err(|err| { anyhow!("Unable to encode certificates in certificate chain - error {err}") @@ -225,11 +214,6 @@ impl X509Certificate { "https://login.microsoftonline.com/{}/oauth2/v2.0/token", tenant_id ) - } else if let Some(tenant_id) = self.tenant_id.as_ref() { - format!( - "https://login.microsoftonline.com/{}/oauth2/v2.0/token", - tenant_id - ) } else { "https://login.microsoftonline.com/common/oauth2/v2.0/token".to_owned() } diff --git a/graph-oauth/src/identity/form_credential.rs b/graph-oauth/src/identity/form_credential.rs deleted file mode 100644 index d351b1cc..00000000 --- a/graph-oauth/src/identity/form_credential.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::auth::OAuthParameter; - -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum ParameterIs { - Required(OAuthParameter), - Optional(OAuthParameter), -} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 7af63775..6b3197be 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -3,7 +3,6 @@ mod authority; mod authorization_serializer; mod credential_store; mod credentials; -pub(crate) mod form_credential; pub use allowed_host_validator::*; pub use authority::*; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index bd6270e7..38b49520 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,7 +3,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - AccessToken, ClientSecretCredential, CredentialBuilder, ResourceOwnerPasswordCredential, + AccessToken, ClientSecretCredential, ResourceOwnerPasswordCredential, TokenRequest, }; use graph_rs_sdk::Graph; From 88b926295b87f49b069f425c896c2d7ef6a68766 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 7 Jul 2023 00:23:59 -0400 Subject: [PATCH 024/118] Updating oauth examples --- .cargo/config.toml | 2 +- Cargo.toml | 4 + examples/oauth/auth_code_grant.rs | 4 +- examples/oauth/auth_code_grant_pkce.rs | 4 +- .../oauth/auth_code_grant_refresh_token.rs | 2 +- examples/oauth/client_credentials.rs | 3 +- examples/oauth/device_code.rs | 22 +- examples/oauth/implicit_grant.rs | 2 +- examples/oauth/main.rs | 2 +- examples/oauth/open_id_connect.rs | 3 +- examples/oauth_certificate/main.rs | 8 +- graph-http/.cargo/config.toml | 2 +- graph-http/src/client.rs | 74 ++++--- graph-http/src/lib.rs | 2 + graph-http/src/pipeline/http_pipeline.rs | 67 ++++++ graph-http/src/pipeline/mod.rs | 3 + graph-oauth/.cargo/config.toml | 2 +- graph-oauth/Cargo.toml | 3 + graph-oauth/src/access_token.rs | 6 +- graph-oauth/src/auth.rs | 123 ++++++----- graph-oauth/src/device_code.rs | 10 +- ...auth_code_authorization_url_parameters.rs} | 56 ++--- ...thorization_code_certificate_credential.rs | 34 ++- .../authorization_code_credential.rs | 34 ++- .../credentials/client_application.rs | 4 +- .../client_certificate_credential.rs | 39 ++-- .../credentials/client_secret_credential.rs | 25 +-- .../confidential_client_application.rs | 16 +- .../credentials/credential_builder.rs | 10 +- .../credentials/device_code_credential.rs | 131 +++++++++-- .../credentials/environment_credential.rs | 41 +++- .../credentials/implicit_credential.rs | 4 +- .../legacy/code_flow_authorization_url.rs | 3 +- .../src/identity/credentials/legacy/mod.rs | 2 +- .../legacy/token_flow_authorization_url.rs | 3 +- graph-oauth/src/identity/credentials/mod.rs | 4 +- .../credentials/open_id_credential.rs | 27 +-- .../credentials/public_client_application.rs | 32 ++- .../resource_owner_password_credential.rs | 41 ++-- .../identity/credentials/token_credential.rs | 81 ++++++- .../src/identity/credentials/token_request.rs | 3 + .../identity/credentials/x509_certificate.rs | 68 +++++- graph-oauth/src/lib.rs | 2 + graph-oauth/src/oauth2_header.rs | 1 + graph-sdk-abstractions/Cargo.toml | 26 +++ graph-sdk-abstractions/src/backing_store.rs | 51 +++++ graph-sdk-abstractions/src/http/mod.rs | 206 ++++++++++++++++++ graph-sdk-abstractions/src/lib.rs | 2 + test-tools/src/oauth_request.rs | 3 +- 49 files changed, 980 insertions(+), 317 deletions(-) create mode 100644 graph-http/src/pipeline/http_pipeline.rs create mode 100644 graph-http/src/pipeline/mod.rs rename graph-oauth/src/identity/credentials/{auth_code_authorization_url.rs => auth_code_authorization_url_parameters.rs} (93%) create mode 100644 graph-oauth/src/oauth2_header.rs create mode 100644 graph-sdk-abstractions/Cargo.toml create mode 100644 graph-sdk-abstractions/src/backing_store.rs create mode 100644 graph-sdk-abstractions/src/http/mod.rs create mode 100644 graph-sdk-abstractions/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 4ae96a70..5ca0cd9b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [env] -USER_AGENT = "graph-rs-sdk/1.1.1" +GRAPH_CLIENT_USER_AGENT = "graph-rs-sdk/1.1.1" diff --git a/Cargo.toml b/Cargo.toml index d476290f..fb06e053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "graph-codegen", "graph-http", "graph-core", + "graph-sdk-abstractions" ] [dependencies] @@ -67,7 +68,10 @@ anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.4" from_as = "0.2.0" +actix = "0.13.0" +actix-rt = "2.8.0" +graph-sdk-abstractions = { path = "./graph-sdk-abstractions" } graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index dbe94fe7..72552293 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::oauth::{ - AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, - ConfidentialClientApplication, CredentialBuilder, TokenRequest, + AccessToken, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, + ConfidentialClientApplication, TokenCredential, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 81a84b71..8be51798 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ - AccessToken, AuthCodeAuthorizationUrl, AuthorizationCodeCredential, - ConfidentialClientApplication, CredentialBuilder, ProofKeyForCodeExchange, TokenRequest, + AccessToken, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, + ConfidentialClientApplication, ProofKeyForCodeExchange, TokenCredential, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant_refresh_token.rs index 9d3ffd5f..3d0f0896 100644 --- a/examples/oauth/auth_code_grant_refresh_token.rs +++ b/examples/oauth/auth_code_grant_refresh_token.rs @@ -1,6 +1,6 @@ use graph_oauth::identity::AuthorizationCodeCredentialBuilder; use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, TokenRequest, + AuthorizationCodeCredential, ConfidentialClientApplication, TokenCredential, TokenRequest, }; // Use a refresh token to get a new access token. diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index f0882816..751e25a3 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -10,7 +10,8 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - AccessToken, ClientSecretCredential, ConfidentialClientApplication, TokenRequest, + AccessToken, ClientSecretCredential, ConfidentialClientApplication, TokenCredential, + TokenRequest, }; // This example shows programmatically getting an access token using the client credentials diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index b84b5978..262c5632 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,3 +1,4 @@ +use graph_oauth::identity::{DeviceCodeCredential, TokenCredential}; use graph_rs_sdk::oauth::{AccessToken, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; @@ -20,6 +21,15 @@ fn get_oauth() -> OAuthSerializer { oauth } +fn device_code_credential() -> DeviceCodeCredential { + let client_id = "CLIENT_ID"; + + DeviceCodeCredential::builder() + .with_scope(vec!["files.read", "offline_access"]) + .with_client_id(client_id) + .build() +} + // When polling to wait on the user to enter a device code you should check the errors // so that you know what to do next. // @@ -32,6 +42,8 @@ async fn poll_for_access_token( interval: u64, message: &str, ) -> GraphResult<serde_json::Value> { + let mut credential = device_code_credential(); + let mut oauth = get_oauth(); oauth.device_code(device_code); @@ -67,7 +79,9 @@ async fn poll_for_access_token( match error { "authorization_pending" => println!("Still waiting on user to sign in"), "authorization_declined" => panic!("user declined to sign in"), - "bad_verification_code" => println!("User is lost\n{message:#?}"), + "bad_verification_code" => { + println!("Bad verification code. Message:\n{message:#?}") + } "expired_token" => panic!("token has expired - user did not sign in"), _ => { panic!("This isn't the error we expected: {error:#?}"); @@ -87,10 +101,8 @@ async fn poll_for_access_token( // The authorization url for device code must be https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode // where tenant can be common, pub async fn device_code() -> GraphResult<()> { - let mut oauth = get_oauth(); - - let mut handler = oauth.build_async().device_code(); - let response = handler.authorization().send().await?; + let mut credential = device_code_credential(); + let response = credential.get_token_async().await?; println!("{:#?}", response); let json: serde_json::Value = response.json().await?; diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index c376850c..dee2c4ad 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -18,7 +18,7 @@ use std::collections::BTreeSet; // // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 use graph_rs_sdk::oauth::{ - CredentialBuilder, ImplicitCredential, Prompt, ResponseMode, ResponseType, + ImplicitCredential, Prompt, ResponseMode, ResponseType, TokenCredential, }; fn oauth_implicit_flow() { diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 5077118d..e6eee006 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -31,7 +31,7 @@ mod signing_keys; use graph_rs_sdk::oauth::{ AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenCredential, TokenRequest, }; diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index b154ac04..da219c21 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,4 +1,4 @@ -use graph_oauth::identity::{ResponseType, TokenRequest}; +use graph_oauth::identity::{ResponseType, TokenCredential, TokenRequest}; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; use url::Url; @@ -67,7 +67,6 @@ async fn handle_redirect( let mut access_token: AccessToken = response.json().await.unwrap(); access_token.enable_pii_logging(true); - // If all went well here we can print out the OAuth config with the Access Token. println!("\n{:#?}\n", access_token); } else { // See if Microsoft Graph returned an error in the Response body diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 3cd560fc..bb1cc990 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,8 +4,8 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, - CredentialBuilder, PKey, TokenRequest, X509Certificate, X509, + AccessToken, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, + TokenCredential, X509Certificate, X509, }; use std::fs::File; use std::io::Read; @@ -86,9 +86,7 @@ pub fn get_confidential_client( let cert = X509::from_pem(certificate.as_slice()).unwrap(); let pkey = PKey::private_key_from_pem(private_key.as_slice()).unwrap(); - let mut x509_certificate = X509Certificate::new(client_id, cert, pkey); - - x509_certificate.with_tenant(tenant_id); + let x509_certificate = X509Certificate::new_with_tenant(client_id, tenant_id, cert, pkey); let credentials = AuthorizationCodeCertificateCredential::builder() .with_authorization_code(authorization_code) diff --git a/graph-http/.cargo/config.toml b/graph-http/.cargo/config.toml index a41675fd..5ca0cd9b 100644 --- a/graph-http/.cargo/config.toml +++ b/graph-http/.cargo/config.toml @@ -1,2 +1,2 @@ [env] -GRAPH_RS_SDK = "1.1.1" +GRAPH_CLIENT_USER_AGENT = "graph-rs-sdk/1.1.1" diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 477186fb..826e56d6 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -7,6 +7,11 @@ use std::ffi::OsStr; use std::fmt::{Debug, Formatter}; use std::time::Duration; +fn user_agent_header_from_env() -> Option<HeaderValue> { + let header = std::option_env!("GRAPH_CLIENT_USER_AGENT")?; + HeaderValue::from_str(&header).ok() +} + #[derive(Clone)] struct ClientConfiguration { access_token: Option<String>, @@ -24,6 +29,10 @@ impl ClientConfiguration { let mut headers: HeaderMap<HeaderValue> = HeaderMap::with_capacity(2); headers.insert(ACCEPT, HeaderValue::from_static("*/*")); + if let Some(user_agent) = user_agent_header_from_env() { + headers.insert(USER_AGENT, user_agent); + } + ClientConfiguration { access_token: None, headers, @@ -129,6 +138,15 @@ impl GraphClientConfiguration { self } + fn retain_agent_header(&self) -> Option<HeaderValue> { + if !self.config.headers.contains_key(USER_AGENT) { + let header = std::env::var(USER_AGENT.as_str()).ok()?; + HeaderValue::from_str(&header).ok() + } else { + None + } + } + pub fn build(self) -> Client { let config = self.clone(); let headers = self.config.headers.clone(); @@ -137,19 +155,8 @@ impl GraphClientConfiguration { .connection_verbose(self.config.connection_verbose) .https_only(self.config.https_only) .min_tls_version(self.config.min_tls_version) - .redirect(Policy::limited(2)); - - if !self.config.headers.contains_key(USER_AGENT) { - let mut headers = self.config.headers.clone(); - if let Ok(user_agent_header) = std::env::var("USER_AGENT") { - if let Ok(header_value) = HeaderValue::from_str(&user_agent_header) { - headers.insert(USER_AGENT, header_value); - builder = builder.default_headers(self.config.headers); - } - } - } else { - builder = builder.default_headers(self.config.headers); - } + .redirect(Policy::limited(2)) + .default_headers(self.config.headers); if let Some(timeout) = self.config.timeout { builder = builder.timeout(timeout); @@ -174,19 +181,8 @@ impl GraphClientConfiguration { .connection_verbose(self.config.connection_verbose) .https_only(self.config.https_only) .min_tls_version(self.config.min_tls_version) - .redirect(Policy::limited(2)); - - if !self.config.headers.contains_key(USER_AGENT) { - let mut headers = self.config.headers.clone(); - if let Ok(user_agent_header) = std::env::var("USER_AGENT") { - if let Ok(header_value) = HeaderValue::from_str(&user_agent_header) { - headers.insert(USER_AGENT, header_value); - builder = builder.default_headers(self.config.headers); - } - } - } else { - builder = builder.default_headers(self.config.headers); - } + .redirect(Policy::limited(2)) + .default_headers(self.config.headers); if let Some(timeout) = self.config.timeout { builder = builder.timeout(timeout); @@ -257,3 +253,29 @@ impl Debug for Client { .finish() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn compile_time_user_agent_header() { + let mut client = GraphClientConfiguration::new() + .access_token("access_token") + .build(); + + assert!(client.builder.config.headers.contains_key(USER_AGENT)); + } + + #[test] + fn update_user_agent_header() { + let mut client = GraphClientConfiguration::new() + .access_token("access_token") + .user_agent(HeaderValue::from_static("user_agent")) + .build(); + + assert!(client.builder.config.headers.contains_key(USER_AGENT)); + let user_agent_header = client.builder.config.headers.get(USER_AGENT).unwrap(); + assert_eq!("user_agent", user_agent_header.to_str().unwrap()); + } +} diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 93c5ffd0..9b2764f6 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -4,6 +4,7 @@ extern crate serde; mod blocking; mod client; mod core; +mod pipeline; mod request_components; mod request_handler; mod resource_identifier; @@ -22,6 +23,7 @@ pub(crate) mod internal { pub use crate::client::*; pub use crate::core::*; pub use crate::io_tools::*; + pub use crate::pipeline::*; pub use crate::request_components::*; pub use crate::request_handler::*; pub use crate::resource_identifier::*; diff --git a/graph-http/src/pipeline/http_pipeline.rs b/graph-http/src/pipeline/http_pipeline.rs new file mode 100644 index 00000000..547d96b2 --- /dev/null +++ b/graph-http/src/pipeline/http_pipeline.rs @@ -0,0 +1,67 @@ +use http::Request; +use serde_json::Value; +use std::error::Error; +use std::sync::Arc; + +pub struct RequestContext { + // ... request context fields +} +// request context impl somewhere + +// Just here as an example. The actual struct/impl would be different. +pub struct SomePolicyResult; + +pub trait HttpPipelinePolicy { + // Modify the request. + fn process_async( + &self, + context: &RequestContext, + request: &mut http::Request<Value>, + pipeline: &[Arc<dyn HttpPipelinePolicy>], + ) -> Result<SomePolicyResult, Box<dyn std::error::Error>>; + + fn process_next_async( + &self, + context: &RequestContext, + request: &mut http::Request<Value>, + pipeline: &[Arc<dyn HttpPipelinePolicy>], + ) -> Result<SomePolicyResult, Box<dyn std::error::Error>> { + pipeline[0].process_async(context, request, &pipeline[1..]) + } +} + +// Example only. Not exact at all. +pub struct ExponentialBackoffRetryPolicy { + // ... retry fields + pub min_retries: u32, +} + +impl HttpPipelinePolicy for ExponentialBackoffRetryPolicy { + fn process_async( + &self, + context: &RequestContext, + request: &mut Request<Value>, + pipeline: &[Arc<dyn HttpPipelinePolicy>], + ) -> Result<SomePolicyResult, Box<dyn Error>> { + // modify request... + + Ok(SomePolicyResult) + } +} + +pub struct ThrottleRetryPolicy { + // ... impl +} + +impl HttpPipelinePolicy for ThrottleRetryPolicy { + fn process_async( + &self, + context: &RequestContext, + request: &mut Request<Value>, + pipeline: &[Arc<dyn HttpPipelinePolicy>], + ) -> Result<SomePolicyResult, Box<dyn Error>> { + // modify request... + + Ok(SomePolicyResult) + } +} diff --git a/graph-http/src/pipeline/mod.rs b/graph-http/src/pipeline/mod.rs new file mode 100644 index 00000000..647ef572 --- /dev/null +++ b/graph-http/src/pipeline/mod.rs @@ -0,0 +1,3 @@ +mod http_pipeline; + +pub use http_pipeline::*; diff --git a/graph-oauth/.cargo/config.toml b/graph-oauth/.cargo/config.toml index a41675fd..5ca0cd9b 100644 --- a/graph-oauth/.cargo/config.toml +++ b/graph-oauth/.cargo/config.toml @@ -1,2 +1,2 @@ [env] -GRAPH_RS_SDK = "1.1.1" +GRAPH_CLIENT_USER_AGENT = "graph-rs-sdk/1.1.1" diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index fca0a1a1..c551d995 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -37,6 +37,9 @@ wry = "0.28.3" uuid = { version = "1.3.1", features = ["v4"] } log = "0.4" pretty_env_logger = "0.4" +tokio = { version = "1.27.0", features = ["full"] } +hyper = { version = "1.0.0-rc.3", features = ["full"] } +http-body-util = "0.1.0-rc.2" graph-error = { path = "../graph-error" } diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 9e741fc5..b4c395d9 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -81,8 +81,8 @@ impl AccessToken { pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> AccessToken { AccessToken { token_type: token_type.into(), - ext_expires_in: Some(expires_in), - expires_in, + ext_expires_in: Some(expires_in.clone()), + expires_in: expires_in.clone(), scope: Some(scope.into()), access_token: access_token.into(), refresh_token: None, @@ -413,7 +413,7 @@ impl<'de> Deserialize<'de> for AccessToken { Ok(AccessToken { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, - expires_in: phantom_access_token.expires_in, + expires_in: phantom_access_token.expires_in.clone(), ext_expires_in: phantom_access_token.ext_expires_in, scope: phantom_access_token.scope, refresh_token: phantom_access_token.refresh_token, diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 73a03055..750885d0 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -6,9 +6,9 @@ use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; -use graph_error::{AF, AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult, AF}; use ring::rand::SecureRandom; -use std::collections::btree_map::BTreeMap; +use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::{BTreeSet, HashMap}; use std::default::Default; use std::fmt; @@ -121,25 +121,20 @@ impl AsRef<str> for OAuthParameter { } } -/// # OAuth -/// -/// OAuth client implementing the OAuth 2.0 and OpenID Connect protocols -/// on Microsoft identity platform. -/// -/// The client supports almost all OAuth 2.0 flows that Microsoft -/// implements as well as the token and code flow specific to the -/// OneDrive api. -/// -/// The OAuth client is strict on what can be used for a specific OAuth -/// flow. This is to ensure that the credentials used in requests include -/// only information that is required or optional for that specific grant -/// and not any other. Even if you accidentally pass a value, such as a nonce, -/// for a grant type that does not use it, any request that is made will not -/// include the nonce regardless. +pub struct OAuth2Client { + headers: HashMap<String, String>, + query_parameters: HashMap<String, String>, + body_parameters: HashMap<String, String>, +} + +impl OAuth2Client { + pub fn new(logger: impl log::Log) {} +} + +/// Serializer for query/x-www-form-urlencoded OAuth requests. /// -/// # Disclaimer -/// Using this API for other resource owners besides Microsoft may work but -/// functionality will more then likely be limited. +/// OAuth Serializer for query/form serialization that supports the OAuth 2.0 and OpenID +/// Connect protocols on Microsoft identity platform. /// /// # Example /// ``` @@ -192,7 +187,9 @@ impl OAuthSerializer { | OAuthParameter::AccessTokenUrl | OAuthParameter::AuthorizationUrl | OAuthParameter::LogoutURL => { - Url::parse(v.as_ref()).unwrap(); + Url::parse(v.as_ref()) + .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) + .unwrap(); } _ => {} } @@ -210,10 +207,10 @@ impl OAuthSerializer { /// # use graph_oauth::oauth::OAuthSerializer; /// # use graph_oauth::oauth::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); - /// let entry = oauth.entry(OAuthParameter::AuthorizationUrl, "https://example.com"); + /// let entry = oauth.entry_with(OAuthParameter::AuthorizationUrl, "https://example.com"); /// assert_eq!(entry, "https://example.com") /// ``` - pub fn entry<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { + pub fn entry_with<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { let v = value.to_string(); match oac { OAuthParameter::RefreshTokenUrl @@ -221,7 +218,8 @@ impl OAuthSerializer { | OAuthParameter::AccessTokenUrl | OAuthParameter::AuthorizationUrl | OAuthParameter::LogoutURL => { - Url::parse(v.as_ref()).map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) + Url::parse(v.as_ref()) + .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) .unwrap(); } _ => {} @@ -232,6 +230,15 @@ impl OAuthSerializer { .or_insert_with(|| v) } + /// A view into a single entry in a map, which may either be vacant or occupied. + /// + /// This `enum` is constructed from the [`entry`] method on [`BTreeMap`]. + /// + /// [`entry`]: BTreeMap::entry + pub fn entry<V: ToString>(&mut self, oac: OAuthParameter) -> Entry<String, String> { + self.credentials.entry(oac.alias().to_string()) + } + /// Get a previously set credential. /// /// # Example @@ -523,7 +530,7 @@ impl OAuthSerializer { self.authorization_code(code.as_str()); } if let Some(state) = value.get_state() { - let _ = self.entry(OAuthParameter::State, state.as_str()); + let _ = self.entry_with(OAuthParameter::State, state.as_str()); } if let Some(session_state) = value.get_session_state() { self.session_state(session_state.as_str()); @@ -1146,7 +1153,7 @@ impl OAuthSerializer { GrantType::TokenFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); self.form_encode_credentials(GrantType::TokenFlow.available_credentials(GrantRequest::Authorization), &mut encoder); let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { @@ -1166,8 +1173,8 @@ impl OAuthSerializer { GrantType::CodeFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthParameter::ResponseType, "code"); - let _ = self.entry(OAuthParameter::ResponseMode, "query"); + let _ = self.entry_with(OAuthParameter::ResponseType, "code"); + let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::Authorization), &mut encoder); let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; @@ -1178,13 +1185,13 @@ impl OAuthSerializer { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthParameter::ResponseType, "token"); - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1192,8 +1199,8 @@ impl OAuthSerializer { GrantType::AuthorizationCode => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthParameter::ResponseType, "code"); - let _ = self.entry(OAuthParameter::ResponseMode, "query"); + let _ = self.entry_with(OAuthParameter::ResponseType, "code"); + let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(GrantRequest::Authorization), &mut encoder); let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; if !url.ends_with('?') { @@ -1204,9 +1211,9 @@ impl OAuthSerializer { } GrantRequest::AccessToken | GrantRequest::RefreshToken => { if request_type == GrantRequest::AccessToken { - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); } else { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(request_type), &mut encoder); Ok(encoder.finish()) @@ -1216,7 +1223,7 @@ impl OAuthSerializer { match request_type { GrantRequest::Authorization => { if !self.scopes.is_empty() { - let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); } self.form_encode_credentials(GrantType::Implicit.available_credentials(GrantRequest::Authorization), &mut encoder); let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; @@ -1247,12 +1254,12 @@ impl OAuthSerializer { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code"); + let _ = self.entry_with(OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code"); self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } @@ -1270,12 +1277,12 @@ impl OAuthSerializer { Ok(url) } GrantRequest::AccessToken => { - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::AccessToken), &mut encoder); Ok(encoder.finish()) } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::RefreshToken), &mut encoder); Ok(encoder.finish()) } @@ -1309,69 +1316,69 @@ impl OAuthSerializer { match grant { GrantType::TokenFlow => { if request_type.eq(&GrantRequest::Authorization) { - let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); } } GrantType::CodeFlow => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthParameter::ResponseType, "code"); - let _ = self.entry(OAuthParameter::ResponseMode, "query"); + let _ = self.entry_with(OAuthParameter::ResponseType, "code"); + let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); } GrantRequest::AccessToken => { - let _ = self.entry(OAuthParameter::ResponseType, "token"); - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); } GrantRequest::RefreshToken => { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } }, GrantType::AuthorizationCode => match request_type { GrantRequest::Authorization => { - let _ = self.entry(OAuthParameter::ResponseType, "code"); - let _ = self.entry(OAuthParameter::ResponseMode, "query"); + let _ = self.entry_with(OAuthParameter::ResponseType, "code"); + let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); } GrantRequest::AccessToken | GrantRequest::RefreshToken => { if request_type == GrantRequest::AccessToken { - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); } else { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } } }, GrantType::Implicit => { if request_type.eq(&GrantRequest::Authorization) && !self.scopes.is_empty() { - let _ = self.entry(OAuthParameter::ResponseType, "token"); + let _ = self.entry_with(OAuthParameter::ResponseType, "token"); } } GrantType::DeviceCode => { if request_type.eq(&GrantRequest::AccessToken) { - let _ = self.entry( + let _ = self.entry_with( OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code", ); } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } } GrantType::OpenId => { if request_type.eq(&GrantRequest::AccessToken) { - let _ = self.entry(OAuthParameter::GrantType, "authorization_code"); + let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } } GrantType::ClientCredentials => { if request_type.eq(&GrantRequest::AccessToken) || request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthParameter::GrantType, "client_credentials"); + let _ = self.entry_with(OAuthParameter::GrantType, "client_credentials"); } } GrantType::ResourceOwnerPasswordCredentials => { if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry(OAuthParameter::GrantType, "refresh_token"); + let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); } else { - let _ = self.entry(OAuthParameter::GrantType, "password"); + let _ = self.entry_with(OAuthParameter::GrantType, "password"); } } } diff --git a/graph-oauth/src/device_code.rs b/graph-oauth/src/device_code.rs index 35d1bf1e..320f41a8 100644 --- a/graph-oauth/src/device_code.rs +++ b/graph-oauth/src/device_code.rs @@ -1,11 +1,14 @@ use serde_json::Value; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::time::Duration; /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct DeviceCode { + /// A long string used to verify the session between the client and the authorization server. + /// The client uses this parameter to request the access token from the authorization server. pub device_code: String, + /// The number of seconds before the device_code and user_code expire. pub expires_in: u64, /// OPTIONAL /// The minimum amount of time in seconds that the client @@ -13,10 +16,15 @@ pub struct DeviceCode { /// value is provided, clients MUST use 5 as the default. #[serde(default = "default_interval")] pub interval: Option<Duration>, + /// User friendly text response that can be used for display purpose. pub message: String, pub user_code: String, + /// Verification URL where the user must navigate to authenticate using the device code + /// and credentials. pub verification_uri: String, pub verification_uri_complete: Option<String>, + /// List of the scopes that would be held by token. + pub scopes: Option<BTreeSet<String>>, #[serde(flatten)] pub additional_fields: HashMap<String, Value>, } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs similarity index 93% rename from graph-oauth/src/identity/credentials/auth_code_authorization_url.rs rename to graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index 19a55a2c..b3f1f107 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -26,7 +26,7 @@ use url::Url; /// /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code #[derive(Clone, Debug)] -pub struct AuthCodeAuthorizationUrl { +pub struct AuthCodeAuthorizationUrlParameters { /// The client (application) ID of the service principal pub(crate) client_id: String, pub(crate) redirect_uri: String, @@ -55,11 +55,11 @@ pub struct AuthCodeAuthorizationUrl { pub(crate) code_challenge_method: Option<String>, } -impl AuthCodeAuthorizationUrl { - pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthCodeAuthorizationUrl { +impl AuthCodeAuthorizationUrlParameters { + pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthCodeAuthorizationUrlParameters { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); - AuthCodeAuthorizationUrl { + AuthCodeAuthorizationUrlParameters { client_id: client_id.as_ref().to_owned(), redirect_uri: redirect_uri.as_ref().to_owned(), authority: Authority::default(), @@ -76,8 +76,8 @@ impl AuthCodeAuthorizationUrl { } } - pub fn builder() -> AuthCodeAuthorizationUrlBuilder { - AuthCodeAuthorizationUrlBuilder::new() + pub fn builder() -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new() } pub fn url(&self) -> AuthorizationResult<Url> { @@ -135,8 +135,7 @@ impl AuthCodeAuthorizationUrl { */ let url = Url::parse(&url_string)?; - let query = url.query() - .or(url.fragment()).ok_or(AF::msg_err( + let query = url.query().or(url.fragment()).ok_or(AF::msg_err( "query | fragment", &format!("No query or fragment returned on redirect, url: {url}"), ))?; @@ -147,10 +146,10 @@ impl AuthCodeAuthorizationUrl { } mod web_view_authenticator { - use crate::identity::{AuthCodeAuthorizationUrl, AuthorizationUrl}; + use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; use crate::web::{InteractiveAuthenticator, InteractiveWebView, InteractiveWebViewOptions}; - impl InteractiveAuthenticator for AuthCodeAuthorizationUrl { + impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { fn interactive_authentication( &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, @@ -182,7 +181,7 @@ mod web_view_authenticator { } } -impl AuthorizationUrl for AuthCodeAuthorizationUrl { +impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { fn redirect_uri(&self) -> AuthorizationResult<Url> { Url::parse(self.redirect_uri.as_str()).map_err(AuthorizationFailure::from) } @@ -240,7 +239,8 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { // Set response_mode if self.response_type.contains(&ResponseType::IdToken) { - if self.response_mode.is_none() || self.response_mode.eq(&Some(ResponseMode::Query)) { + if self.response_mode.is_none() || self.response_mode.eq(&Some(ResponseMode::Query)) + { serializer.response_mode(ResponseMode::Fragment.as_ref()); } else if let Some(response_mode) = self.response_mode.as_ref() { serializer.response_mode(response_mode.as_ref()); @@ -309,22 +309,22 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrl { } #[derive(Clone)] -pub struct AuthCodeAuthorizationUrlBuilder { - authorization_url: AuthCodeAuthorizationUrl, +pub struct AuthCodeAuthorizationUrlParameterBuilder { + authorization_url: AuthCodeAuthorizationUrlParameters, } -impl Default for AuthCodeAuthorizationUrlBuilder { +impl Default for AuthCodeAuthorizationUrlParameterBuilder { fn default() -> Self { Self::new() } } -impl AuthCodeAuthorizationUrlBuilder { - pub fn new() -> AuthCodeAuthorizationUrlBuilder { +impl AuthCodeAuthorizationUrlParameterBuilder { + pub fn new() -> AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); - AuthCodeAuthorizationUrlBuilder { - authorization_url: AuthCodeAuthorizationUrl { + AuthCodeAuthorizationUrlParameterBuilder { + authorization_url: AuthCodeAuthorizationUrlParameters { client_id: String::with_capacity(32), redirect_uri: String::new(), authority: Authority::default(), @@ -426,7 +426,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// /// Providing a scope of `id_token` automatically adds [ResponseType::IdToken] /// and generates a secure nonce value. - /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] + /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { self.authorization_url.scope.extend( scope @@ -474,7 +474,7 @@ impl AuthCodeAuthorizationUrlBuilder { /// /// Including `id_token` also requires a nonce parameter. /// This is generated automatically. - /// See [AuthCodeAuthorizationUrlBuilder::with_nonce_generated] + /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; self.authorization_url @@ -544,7 +544,7 @@ impl AuthCodeAuthorizationUrlBuilder { self } - pub fn build(&self) -> AuthCodeAuthorizationUrl { + pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { self.authorization_url.clone() } @@ -559,7 +559,7 @@ mod test { #[test] fn serialize_uri() { - let authorizer = AuthCodeAuthorizationUrl::builder() + let authorizer = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) @@ -571,7 +571,7 @@ mod test { #[test] fn url_with_host() { - let authorizer = AuthCodeAuthorizationUrl::builder() + let authorizer = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) @@ -583,7 +583,7 @@ mod test { #[test] fn response_mode_set() { - let url = AuthCodeAuthorizationUrl::builder() + let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) @@ -599,7 +599,7 @@ mod test { #[test] fn response_mode_not_set() { - let url = AuthCodeAuthorizationUrl::builder() + let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) @@ -613,7 +613,7 @@ mod test { #[test] fn multi_response_type_set() { - let url = AuthCodeAuthorizationUrl::builder() + let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) @@ -629,7 +629,7 @@ mod test { #[test] fn generate_nonce() { - let url = AuthCodeAuthorizationUrl::builder() + let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https::/localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 853d993f..882db6bb 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,8 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - AuthCodeAuthorizationUrl, AuthCodeAuthorizationUrlBuilder, Authority, AuthorizationSerializer, - AzureAuthorityHost, TokenCredential, TokenCredentialOptions, TokenRequest, - CLIENT_ASSERTION_TYPE, + AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, + AuthorizationSerializer, AzureAuthorityHost, TokenCredential, TokenCredentialOptions, + TokenRequest, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; @@ -97,19 +97,13 @@ impl AuthorizationCodeCertificateCredential { AuthorizationCodeCertificateCredentialBuilder::new() } - pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlBuilder { - AuthCodeAuthorizationUrlBuilder::new() + pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new() } } #[async_trait] -impl TokenRequest for AuthorizationCodeCertificateCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { +impl TokenCredential for AuthorizationCodeCertificateCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -204,12 +198,14 @@ impl AuthorizationSerializer for AuthorizationCodeCertificateCredential { "Either authorization code or refresh token is required", ) } -} -impl TokenCredential for AuthorizationCodeCertificateCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } } #[derive(Clone)] @@ -263,9 +259,11 @@ impl AuthorizationCodeCertificateCredentialBuilder { certificate_assertion: &X509Certificate, ) -> anyhow::Result<&mut Self> { if let Some(tenant_id) = self.credential.authority.tenant_id() { - self.with_client_assertion(certificate_assertion.sign(Some(tenant_id.clone()))?); + self.with_client_assertion( + certificate_assertion.sign_with_tenant(Some(tenant_id.clone()))?, + ); } else { - self.with_client_assertion(certificate_assertion.sign(None)?); + self.with_client_assertion(certificate_assertion.sign_with_tenant(None)?); } Ok(self) } @@ -284,8 +282,8 @@ impl AuthorizationCodeCertificateCredentialBuilder { } } -impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCertificateCredentialBuilder { - fn from(value: AuthCodeAuthorizationUrl) -> Self { +impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCertificateCredentialBuilder { + fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); let _ = builder.with_redirect_uri(value.redirect_uri); builder diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index c2c37d6c..76676f6e 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,9 +1,9 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - AuthCodeAuthorizationUrl, Authority, AuthorizationSerializer, AzureAuthorityHost, + AuthCodeAuthorizationUrlParameters, Authority, AuthorizationSerializer, AzureAuthorityHost, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; -use crate::oauth::AuthCodeAuthorizationUrlBuilder; +use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; use reqwest::IntoUrl; @@ -95,19 +95,13 @@ impl AuthorizationCodeCredential { AuthorizationCodeCredentialBuilder::new() } - pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlBuilder { - AuthCodeAuthorizationUrlBuilder::new() + pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new() } } #[async_trait] -impl TokenRequest for AuthorizationCodeCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for AuthorizationCodeCredential { +impl TokenCredential for AuthorizationCodeCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -198,15 +192,17 @@ impl AuthorizationSerializer for AuthorizationCodeCredential { ) } - fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) - } -} - -impl TokenCredential for AuthorizationCodeCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } } #[derive(Clone)] @@ -270,8 +266,8 @@ impl AuthorizationCodeCredentialBuilder { } } -impl From<AuthCodeAuthorizationUrl> for AuthorizationCodeCredentialBuilder { - fn from(value: AuthCodeAuthorizationUrl) -> Self { +impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCredentialBuilder { + fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); let _ = builder.with_redirect_uri(value.redirect_uri); builder diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs index 3f54e0fe..4862f552 100644 --- a/graph-oauth/src/identity/credentials/client_application.rs +++ b/graph-oauth/src/identity/credentials/client_application.rs @@ -1,9 +1,9 @@ -use crate::identity::{CredentialStoreType, TokenRequest}; +use crate::identity::{CredentialStoreType, TokenCredential}; use crate::oauth::AccessToken; use async_trait::async_trait; #[async_trait] -pub trait ClientApplication: TokenRequest { +pub trait ClientApplication: TokenCredential { fn get_credential_from_store(&self) -> &CredentialStoreType; fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType); diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 40d2ccfd..176da938 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AF, AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; use url::Url; @@ -68,26 +68,22 @@ impl ClientCertificateCredential { } #[async_trait] -impl TokenRequest for ClientCertificateCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for ClientCertificateCredential { +impl TokenCredential for ClientCertificateCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AF::msg_err("access_token_url", "Internal Error"), - )?; + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; Url::parse(uri.as_str()).map_err(AF::from) } else { - let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AF::msg_err("refresh_token_url", "Internal Error"), - )?; + let uri = self + .serializer + .get(OAuthParameter::RefreshTokenUrl) + .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; Url::parse(uri.as_str()).map_err(AF::from) } } @@ -151,12 +147,14 @@ impl AuthorizationSerializer for ClientCertificateCredential { ) }; } -} -impl TokenCredential for ClientCertificateCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } } pub struct ClientCertificateCredentialBuilder { @@ -180,14 +178,11 @@ impl ClientCertificateCredentialBuilder { } #[cfg(feature = "openssl")] - pub fn with_certificate( - &mut self, - certificate: &X509Certificate, - ) -> anyhow::Result<&mut Self> { + pub fn with_certificate(&mut self, certificate: &X509Certificate) -> anyhow::Result<&mut Self> { if let Some(tenant_id) = self.credential.authority.tenant_id() { - self.with_client_assertion(certificate.sign(Some(tenant_id.clone()))?); + self.with_client_assertion(certificate.sign_with_tenant(Some(tenant_id.clone()))?); } else { - self.with_client_assertion(certificate.sign(None)?); + self.with_client_assertion(certificate.sign_with_tenant(None)?); } Ok(self) } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index d67c4619..57925be1 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -4,6 +4,7 @@ use crate::identity::{ ClientCredentialsAuthorizationUrlBuilder, TokenCredential, TokenRequest, }; use crate::oauth::TokenCredentialOptions; +use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -82,13 +83,8 @@ impl ClientSecretCredential { } } -impl TokenRequest for ClientSecretCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for ClientSecretCredential { +#[async_trait] +impl TokenCredential for ClientSecretCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -121,16 +117,17 @@ impl AuthorizationSerializer for ClientSecretCredential { .as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) } - /// - fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) - } -} - -impl TokenCredential for ClientSecretCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } } pub struct ClientSecretCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index d8bdea32..22a17376 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -38,7 +38,8 @@ impl ConfidentialClientApplication { } } -impl AuthorizationSerializer for ConfidentialClientApplication { +#[async_trait] +impl TokenCredential for ConfidentialClientApplication { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } @@ -46,10 +47,11 @@ impl AuthorizationSerializer for ConfidentialClientApplication { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { self.credential.form_urlencode() } -} -#[async_trait] -impl TokenRequest for ConfidentialClientApplication { + fn client_id(&self) -> &String { + self.credential.client_id() + } + fn token_credential_options(&self) -> &TokenCredentialOptions { &self.token_credential_options } @@ -115,12 +117,6 @@ impl TokenRequest for ConfidentialClientApplication { } } -impl TokenCredential for ConfidentialClientApplication { - fn client_id(&self) -> &String { - self.credential.client_id() - } -} - impl ClientApplication for ConfidentialClientApplication { fn get_credential_from_store(&self) -> &CredentialStoreType { match self.credential_store.token_cache_provider() { diff --git a/graph-oauth/src/identity/credentials/credential_builder.rs b/graph-oauth/src/identity/credentials/credential_builder.rs index 3290ae44..3c8c5366 100644 --- a/graph-oauth/src/identity/credentials/credential_builder.rs +++ b/graph-oauth/src/identity/credentials/credential_builder.rs @@ -12,11 +12,15 @@ macro_rules! credential_builder_impl { /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { - self.credential.authority = crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.authority = + crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); self } - pub fn with_authority<T: Into<crate::identity::Authority>>(&mut self, authority: T) -> &mut Self { + pub fn with_authority<T: Into<crate::identity::Authority>>( + &mut self, + authority: T, + ) -> &mut Self { self.credential.authority = authority.into(); self } @@ -41,5 +45,5 @@ macro_rules! credential_builder_impl { self.credential.clone() } } - } + }; } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 2e039a5c..10a687aa 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,18 +1,19 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, + Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, + TokenCredentialOptions, TokenRequest, }; -use crate::oauth::DeviceCode; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use crate::oauth::{DeviceCode, PublicClientApplication}; +use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult, AF}; +use reqwest::Response; use std::collections::HashMap; +use std::time::Duration; use url::Url; +use wry::http; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; -credential_builder_impl!( - DeviceCodeCredentialBuilder, - DeviceCodeCredential -); +credential_builder_impl!(DeviceCodeCredentialBuilder, DeviceCodeCredential); /// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, /// or a printer. To enable this flow, the device has the user visit a webpage in a browser on @@ -69,12 +70,95 @@ impl DeviceCodeCredential { self } + pub fn with_device_code<T: AsRef<str>>(&mut self, device_code: T) -> &mut Self { + self.refresh_token = None; + self.device_code = Some(device_code.as_ref().to_owned()); + self + } + + /* + pub async fn poll_async(&mut self, buffer: Option<usize>) -> tokio::sync::mpsc::Receiver<GraphResult<http::Response<serde_json::Value>>> { + let (sender, receiver) = { + if let Some(buffer) = buffer { + tokio::sync::mpsc::channel(buffer) + } else { + tokio::sync::mpsc::channel(100) + } + }; + + let mut credential = self.clone(); + let mut application = PublicClientApplication::from(self.clone()); + + tokio::spawn(async move { + let response = application.get_token_async().await + .map_err(GraphFailure::from); + + match response { + Ok(response) => { + let status = response.status(); + + let body: serde_json::Value = response.json().await?; + println!("{body:#?}"); + + let device_code = body["device_code"].as_str().unwrap(); + let interval = body["interval"].as_u64().unwrap(); + let message = body["message"].as_str().unwrap(); + credential.with_device_code(device_code); + let mut application = PublicClientApplication::from(credential); + + if !status.is_success() { + loop { + // Wait the amount of seconds that interval is. + std::thread::sleep(Duration::from_secs(interval.clone())); + + let response = application.get_token_async().await + .map_err(GraphFailure::from).unwrap(); + + let status = response.status(); + println!("{response:#?}"); + + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); + + if status.is_success() { + sender.send_timeout(Ok(body), Duration::from_secs(60)); + } else { + let option_error = body["error"].as_str(); + + if let Some(error) = option_error { + match error { + "authorization_pending" => println!("Still waiting on user to sign in"), + "authorization_declined" => panic!("user declined to sign in"), + "bad_verification_code" => println!("Bad verification code. Message:\n{message:#?}"), + "expired_token" => panic!("token has expired - user did not sign in"), + _ => { + panic!("This isn't the error we expected: {error:#?}"); + } + } + } else { + // Body should have error or we should bail. + panic!("Crap hit the fan"); + } + } + } + } + } + Err(err) => { + sender.send_timeout(Err(err), Duration::from_secs(60)); + } + } + }); + + return receiver; + } + */ + pub fn builder() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new() } } -impl AuthorizationSerializer for DeviceCodeCredential { +impl TokenCredential for DeviceCodeCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -140,21 +224,31 @@ impl AuthorizationSerializer for DeviceCodeCredential { vec![], vec![ OAuthParameter::ClientId, - OAuthParameter::ClientSecret, + OAuthParameter::DeviceCode, OAuthParameter::Scope, OAuthParameter::GrantType, ], ); } - AuthorizationFailure::msg_result( - format!( - "{} or {}", - OAuthParameter::DeviceCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - "Either device code or refresh token is required", - ) + self.serializer.grant_type(DEVICE_CODE_GRANT_TYPE); + + return self.serializer.as_credential_map( + vec![], + vec![ + OAuthParameter::ClientId, + OAuthParameter::Scope, + OAuthParameter::GrantType, + ], + ); + } + + fn client_id(&self) -> &String { + &self.client_id + } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options } } @@ -213,10 +307,9 @@ mod test { #[test] #[should_panic] - fn no_device_code() { + fn no_scope() { let mut credential = DeviceCodeCredential::builder() .with_client_id("CLIENT_ID") - .with_scope(vec!["scope"]) .build(); let _ = credential.form_urlencode().unwrap(); diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 5df81bb2..daba2530 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -39,7 +39,11 @@ impl EnvironmentCredential { let tenant_id = option_env!("AZURE_TENANT_ID"); let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_client_secret = option_env!("AZURE_CLIENT_SECRET").ok_or(VarError::NotPresent)?; - EnvironmentCredential::client_secret_env(tenant_id.map(|s| s.to_owned()), azure_client_id.to_owned(), azure_client_secret.to_owned()) + EnvironmentCredential::client_secret_env( + tenant_id.map(|s| s.to_owned()), + azure_client_id.to_owned(), + azure_client_secret.to_owned(), + ) } fn try_azure_client_secret_runtime_env() -> Result<ConfidentialClientApplication, VarError> { @@ -49,7 +53,11 @@ impl EnvironmentCredential { EnvironmentCredential::client_secret_env(tenant_id, azure_client_id, azure_client_secret) } - fn client_secret_env(tenant_id: Option<String>, azure_client_id: String, azure_client_secret: String) -> Result<ConfidentialClientApplication, VarError> { + fn client_secret_env( + tenant_id: Option<String>, + azure_client_id: String, + azure_client_secret: String, + ) -> Result<ConfidentialClientApplication, VarError> { match tenant_id { Some(tenant_id) => Ok(ConfidentialClientApplication::new( ClientSecretCredential::new_with_tenant( @@ -59,12 +67,12 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), + .map_err(|_| VarError::NotPresent)?), None => Ok(ConfidentialClientApplication::new( ClientSecretCredential::new(azure_client_id, azure_client_secret), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), + .map_err(|_| VarError::NotPresent)?), } } @@ -73,7 +81,12 @@ impl EnvironmentCredential { let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_username = option_env!("AZURE_USERNAME").ok_or(VarError::NotPresent)?; let azure_password = option_env!("AZURE_PASSWORD").ok_or(VarError::NotPresent)?; - EnvironmentCredential::username_password_env(tenant_id.map(|s| s.to_owned()), azure_client_id.to_owned(), azure_username.to_owned(), azure_password.to_owned()) + EnvironmentCredential::username_password_env( + tenant_id.map(|s| s.to_owned()), + azure_client_id.to_owned(), + azure_username.to_owned(), + azure_password.to_owned(), + ) } fn try_username_password_runtime_env() -> Result<PublicClientApplication, VarError> { @@ -81,10 +94,20 @@ impl EnvironmentCredential { let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_username = std::env::var(AZURE_USERNAME)?; let azure_password = std::env::var(AZURE_PASSWORD)?; - EnvironmentCredential::username_password_env(tenant_id, azure_client_id, azure_username, azure_password) + EnvironmentCredential::username_password_env( + tenant_id, + azure_client_id, + azure_username, + azure_password, + ) } - fn username_password_env(tenant_id: Option<String>, azure_client_id: String, azure_username: String, azure_password: String) -> Result<PublicClientApplication, VarError> { + fn username_password_env( + tenant_id: Option<String>, + azure_client_id: String, + azure_username: String, + azure_password: String, + ) -> Result<PublicClientApplication, VarError> { match tenant_id { Some(tenant_id) => Ok(PublicClientApplication::new( ResourceOwnerPasswordCredential::new_with_tenant( @@ -95,7 +118,7 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), + .map_err(|_| VarError::NotPresent)?), None => Ok(PublicClientApplication::new( ResourceOwnerPasswordCredential::new( azure_client_id, @@ -104,7 +127,7 @@ impl EnvironmentCredential { ), Default::default(), ) - .map_err(|_| VarError::NotPresent)?), + .map_err(|_| VarError::NotPresent)?), } } } diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 13a9f83e..7bbe13f7 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,7 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType, -}; +use crate::identity::{Authority, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType}; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index d01d1b38..2352f331 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -113,8 +113,7 @@ impl CodeFlowAuthorizationUrlBuilder { } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.authorization_url.scope = - scope.into_iter().map(|s| s.to_string()).collect(); + self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } diff --git a/graph-oauth/src/identity/credentials/legacy/mod.rs b/graph-oauth/src/identity/credentials/legacy/mod.rs index 0926e91a..f2d527c5 100644 --- a/graph-oauth/src/identity/credentials/legacy/mod.rs +++ b/graph-oauth/src/identity/credentials/legacy/mod.rs @@ -1,5 +1,5 @@ -mod code_flow_credential; mod code_flow_authorization_url; +mod code_flow_credential; mod token_flow_authorization_url; pub use code_flow_authorization_url::*; diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index e0afc00d..563bd164 100644 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -99,8 +99,7 @@ impl TokenFlowAuthorizationUrlBuilder { } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.authorization_url.scope = - scope.into_iter().map(|s| s.to_string()).collect(); + self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 3a17ea60..dbf29b68 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -4,7 +4,7 @@ mod credential_builder; pub mod legacy; mod as_query; -mod auth_code_authorization_url; +mod auth_code_authorization_url_parameters; mod authorization_code_certificate_credential; mod authorization_code_credential; mod client_application; @@ -34,7 +34,7 @@ mod token_request; mod x509_certificate; pub use as_query::*; -pub use auth_code_authorization_url::*; +pub use auth_code_authorization_url_parameters::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use client_application::*; diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 15393959..1fc24246 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,8 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, - OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, - TokenRequest, + Authority, AuthorizationSerializer, AzureAuthorityHost, OpenIdAuthorizationUrl, + ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; use crate::oauth::OpenIdAuthorizationUrlBuilder; use async_trait::async_trait; @@ -97,13 +96,7 @@ impl OpenIdCredential { } #[async_trait] -impl TokenRequest for OpenIdCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for OpenIdCredential { +impl TokenCredential for OpenIdCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); @@ -194,15 +187,17 @@ impl AuthorizationSerializer for OpenIdCredential { ) } - fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) - } -} - -impl TokenCredential for OpenIdCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.client_id.clone(), self.client_secret.clone())) + } } #[derive(Clone)] diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 891bab1d..9f74648c 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,6 +1,6 @@ use crate::identity::{ - AuthorizationSerializer, AzureAuthorityHost, ResourceOwnerPasswordCredential, TokenCredential, - TokenCredentialOptions, TokenRequest, + AuthorizationSerializer, AzureAuthorityHost, DeviceCodeCredential, + ResourceOwnerPasswordCredential, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::AuthorizationResult; @@ -35,7 +35,8 @@ impl PublicClientApplication { } } -impl AuthorizationSerializer for PublicClientApplication { +#[async_trait] +impl TokenCredential for PublicClientApplication { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } @@ -43,10 +44,11 @@ impl AuthorizationSerializer for PublicClientApplication { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { self.credential.form_urlencode() } -} -#[async_trait] -impl TokenRequest for PublicClientApplication { + fn client_id(&self) -> &String { + self.credential.client_id() + } + fn token_credential_options(&self) -> &TokenCredentialOptions { &self.token_credential_options } @@ -111,14 +113,22 @@ impl TokenRequest for PublicClientApplication { } } -impl TokenCredential for PublicClientApplication { - fn client_id(&self) -> &String { - self.credential.client_id() +impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { + fn from(value: ResourceOwnerPasswordCredential) -> Self { + PublicClientApplication { + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), + token_credential_options: value.token_credential_options.clone(), + credential: Box::new(value), + } } } -impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { - fn from(value: ResourceOwnerPasswordCredential) -> Self { +impl From<DeviceCodeCredential> for PublicClientApplication { + fn from(value: DeviceCodeCredential) -> Self { PublicClientApplication { http_client: ClientBuilder::new() .min_tls_version(Version::TLS_1_2) diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 2180b9eb..f2cf88c3 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; -use graph_error::{AF, AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; use url::Url; @@ -75,20 +75,15 @@ impl ResourceOwnerPasswordCredential { } #[async_trait] -impl TokenRequest for ResourceOwnerPasswordCredential { - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options - } -} - -impl AuthorizationSerializer for ResourceOwnerPasswordCredential { +impl TokenCredential for ResourceOwnerPasswordCredential { fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AF::msg_err("access_token_url", "Internal Error"), - )?; + let uri = self + .serializer + .get(OAuthParameter::AccessTokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; Url::parse(uri.as_str()).map_err(AF::from) } @@ -116,15 +111,17 @@ impl AuthorizationSerializer for ResourceOwnerPasswordCredential { ) } - fn basic_auth(&self) -> Option<(String, String)> { - Some((self.username.to_string(), self.password.to_string())) - } -} - -impl TokenCredential for ResourceOwnerPasswordCredential { fn client_id(&self) -> &String { &self.client_id } + + fn token_credential_options(&self) -> &TokenCredentialOptions { + &self.token_credential_options + } + + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.username.to_string(), self.password.to_string())) + } } #[derive(Clone)] @@ -182,7 +179,13 @@ impl ResourceOwnerPasswordCredentialBuilder { authority: T, ) -> AuthorizationResult<&mut Self> { let authority = authority.into(); - if vec![Authority::Common, Authority::Consumers, Authority::AzureActiveDirectory].contains(&authority) { + if vec![ + Authority::Common, + Authority::Consumers, + Authority::AzureActiveDirectory, + ] + .contains(&authority) + { return AF::msg_result( "tenant_id", "Authority Azure Active Directory, common, and consumers are not supported authentication contexts for ROPC" @@ -195,7 +198,7 @@ impl ResourceOwnerPasswordCredentialBuilder { { return AuthorizationFailure::msg_result( "tenant_id", - "ADFS, common, and consumers are not supported authentication contexts for ROPC" + "ADFS, common, and consumers are not supported authentication contexts for ROPC", ); } diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 9c906bb7..3bfe4deb 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -1,5 +1,82 @@ -use crate::identity::{AuthorizationSerializer, TokenRequest}; +use crate::identity::{ + AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, +}; +use async_trait::async_trait; +use graph_error::AuthorizationResult; +use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use reqwest::tls::Version; +use reqwest::ClientBuilder; +use std::collections::HashMap; +use url::Url; -pub trait TokenCredential: AuthorizationSerializer + TokenRequest { +#[async_trait] +pub trait TokenCredential { + fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn client_id(&self) -> &String; + fn token_credential_options(&self) -> &TokenCredentialOptions; + + fn basic_auth(&self) -> Option<(String, String)> { + None + } + + fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let options = self.token_credential_options().clone(); + let uri = self.uri(&options.azure_authority_host)?; + let form = self.form_urlencode()?; + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + + let basic_auth = self.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .headers(headers) + .form(&form) + .send()?) + } else { + Ok(http_client.post(uri).form(&form).send()?) + } + } + + async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { + let options = self.token_credential_options().clone(); + let uri = self.uri(&options.azure_authority_host)?; + let form = self.form_urlencode()?; + let http_client = ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + + let basic_auth = self.basic_auth(); + if let Some((client_identifier, secret)) = basic_auth { + Ok(http_client + .post(uri) + .basic_auth(client_identifier, Some(secret)) + .headers(headers) + .form(&form) + .send() + .await?) + } else { + Ok(http_client + .post(uri) + .headers(headers) + .form(&form) + .send() + .await?) + } + } } diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index a2c719a1..fa3b6465 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,8 +1,11 @@ +use crate::identity::{AzureAuthorityHost, TokenCredential}; use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; +use graph_error::AuthorizationResult; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; +use url::Url; #[async_trait] pub trait TokenRequest: AuthorizationSerializer { diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 885d2d55..bd4fa90d 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -41,6 +41,7 @@ fn thumbprint(cert: &X509) -> anyhow::Result<String> { /// https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-client-assertions pub struct X509Certificate { client_id: String, + tenant_id: Option<String>, claims: Option<HashMap<String, String>>, extend_claims: bool, certificate: X509, @@ -54,6 +55,26 @@ impl X509Certificate { pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { Self { client_id: client_id.as_ref().to_owned(), + tenant_id: None, + claims: None, + extend_claims: true, + certificate, + certificate_chain: false, + pkey: private_key, + parsed_pkcs12: None, + uuid: Uuid::new_v4(), + } + } + + pub fn new_with_tenant<T: AsRef<str>>( + client_id: T, + tenant_id: T, + certificate: X509, + private_key: PKey<Private>, + ) -> Self { + Self { + client_id: client_id.as_ref().to_owned(), + tenant_id: Some(tenant_id.as_ref().to_owned()), claims: None, extend_claims: true, certificate, @@ -83,6 +104,38 @@ impl X509Certificate { Ok(Self { client_id: client_id.as_ref().to_owned(), + tenant_id: None, + claims: None, + extend_claims: true, + certificate, + certificate_chain: true, + pkey: private_key.clone(), + parsed_pkcs12: Some(parsed_pkcs12), + uuid: Uuid::new_v4(), + }) + } + + pub fn new_from_pass_with_tenant<T: AsRef<str>>( + client_id: T, + tenant_id: T, + pass: T, + certificate: X509, + ) -> anyhow::Result<Self> { + let der = encode_cert(&certificate)?; + let parsed_pkcs12 = + Pkcs12::from_der(&URL_SAFE_NO_PAD.decode(der)?)?.parse2(pass.as_ref())?; + + let _ = parsed_pkcs12.cert.as_ref().ok_or(anyhow::Error::msg( + "No certificate found after parsing Pkcs12 using pass", + ))?; + + let private_key = parsed_pkcs12.pkey.as_ref().ok_or(anyhow::Error::msg( + "No private key found after parsing Pkcs12 using pass", + ))?; + + Ok(Self { + client_id: client_id.as_ref().to_owned(), + tenant_id: Some(tenant_id.as_ref().to_owned()), claims: None, extend_claims: true, certificate, @@ -271,11 +324,22 @@ impl X509Certificate { Ok(signed_client_assertion) */ + pub fn sign(&self) -> anyhow::Result<String> { + let token = self.base64_token(self.tenant_id.clone())?; + + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; + signer.set_rsa_padding(Padding::PKCS1)?; + signer.update(token.as_str().as_bytes())?; + let signature = URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + + Ok(format!("{token}.{signature}")) + } + /// Get the signed client assertion. /// /// The signature is a Base64 Url encoded (No Pad) JWT Header and Payload signed with the private key using SHA_256 /// and RSA padding PKCS1 - pub fn sign(&self, tenant_id: Option<String>) -> anyhow::Result<String> { + pub fn sign_with_tenant(&self, tenant_id: Option<String>) -> anyhow::Result<String> { let token = self.base64_token(tenant_id)?; let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; @@ -325,6 +389,6 @@ mod test { let private_key = PKey::private_key_from_pem(private_key_bytes).unwrap(); let certificate = X509Certificate::new("client_id", cert, private_key); - assert!(certificate.sign(None).is_ok()); + assert!(certificate.sign_with_tenant(None).is_ok()); } } diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 3c70bc18..2f26c6d0 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -69,6 +69,7 @@ mod discovery; mod grants; mod id_token; pub mod jwt; +mod oauth2_header; mod oauth_error; pub mod identity; @@ -88,6 +89,7 @@ pub mod oauth { pub use crate::grants::GrantType; pub use crate::id_token::IdToken; pub use crate::identity::*; + pub use crate::oauth2_header::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; } diff --git a/graph-oauth/src/oauth2_header.rs b/graph-oauth/src/oauth2_header.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/graph-oauth/src/oauth2_header.rs @@ -0,0 +1 @@ + diff --git a/graph-sdk-abstractions/Cargo.toml b/graph-sdk-abstractions/Cargo.toml new file mode 100644 index 00000000..f38128ad --- /dev/null +++ b/graph-sdk-abstractions/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "graph-sdk-abstractions" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# actix = "0.13.0" +# actix-rt = "2.8.0" +http = "0.2.9" +reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +tokio = { version = "1", features = ["full"] } +futures = "0.3" +tower = { version="0.4.13", features=["full"] } +tower-service = "0.3.2" +tower-layer = "0.3.2" +pin-project = "1.1.1" + +[features] +default = ["native-tls"] +native-tls = ["reqwest/native-tls"] +rustls-tls = ["reqwest/rustls-tls"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] diff --git a/graph-sdk-abstractions/src/backing_store.rs b/graph-sdk-abstractions/src/backing_store.rs new file mode 100644 index 00000000..cd90a97c --- /dev/null +++ b/graph-sdk-abstractions/src/backing_store.rs @@ -0,0 +1,51 @@ +pub struct BackingStore; + +/* +use std::marker::PhantomData; +use actix::{Actor, Context, Handler}; +use actix::dev::{MessageResponse, OneshotSender}; +use actix::prelude::Message; + +pub struct BackingStore { + response: Responses +} + +impl BackingStore { + pub fn new(t: Responses) -> BackingStore { + BackingStore { + response: t + } + } +} + +impl Actor for BackingStore { + type Context = Context<Self>; +} + +impl<M: Message<Result = Responses>> Handler<M> for BackingStore { + type Result = Responses; + + fn handle(&mut self, msg: M, ctx: &mut Self::Context) -> Self::Result { + self.response.clone() + } +} + +#[derive(Clone)] +pub enum Responses { + AccessToken(String), + RefreshToken(String) +} + +impl<A, M> MessageResponse<A, M> for Responses + where + A: Actor, + M: Message<Result = Responses>, +{ + fn handle(self, ctx: &mut A::Context, tx: Option<OneshotSender<M::Result>>) { + if let Some(tx) = tx { + tx.send(self); + } + } +} + + */ diff --git a/graph-sdk-abstractions/src/http/mod.rs b/graph-sdk-abstractions/src/http/mod.rs new file mode 100644 index 00000000..92995d95 --- /dev/null +++ b/graph-sdk-abstractions/src/http/mod.rs @@ -0,0 +1,206 @@ +use futures::future; +use futures::future::Ready; +use http::header::RETRY_AFTER; +use pin_project::pin_project; +use reqwest::{Request, Response}; +use std::{ + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio::time::Sleep; +use tower::retry::{Policy, Retry, RetryLayer}; +use tower::{BoxError, ServiceBuilder}; +use tower_service::Service; + +#[derive(Debug, Clone)] +pub struct RetryAfterPolicy(Duration); + +impl RetryAfterPolicy { + pub fn new(timeout: Duration) -> RetryAfterPolicy { + RetryAfterPolicy(timeout) + } + + pub async fn suspend(self) -> Ready<Self> { + tokio::time::sleep(self.0).await; + return futures::future::ready(self); + } +} + +impl Default for RetryAfterPolicy { + fn default() -> Self { + RetryAfterPolicy::new(Duration::from_secs(0)) + } +} + +impl<E> Policy<reqwest::Request, reqwest::Response, E> for RetryAfterPolicy { + type Future = Ready<Self>; + + fn retry(&self, req: &Request, result: Result<&Response, &E>) -> Option<Self::Future> { + println!("INSIDE THROTTLE RETRY POLICY"); + dbg!("INSIDE THROTTLE RETRY POLICY"); + if let Ok(response) = result.as_ref() { + let header = response.headers().get(RETRY_AFTER)?; + let value_str = header.to_str().ok()?; + let sec: u64 = value_str.parse().ok()?; + return Some(tower::retry::future::ResponseFuture { + request: req, + retry: (), + state: State::Retrying, + }); + } + None + } + + fn clone_request(&self, req: &Request) -> Option<Request> { + req.try_clone() + } +} + +// https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md +pub struct HttpClient; + +impl HttpClient { + pub fn new(client: reqwest::Client) -> reqwest::Client { + let service = ServiceBuilder::new() + .retry(RetryAfterPolicy::new()) + .service(client) + .into_inner(); + return service; + } +} + +#[cfg(test)] +mod test { + use super::*; + use futures::SinkExt; + use http::StatusCode; + use reqwest::Url; + use tower::ServiceExt; + + #[tokio::test] + async fn test_throttle_retry() { + let mut service = ServiceBuilder::new() + .layer(RetryLayer::new(RetryAfterPolicy::default())) + .service(reqwest::Client::new()); + let res: Result<reqwest::Response, reqwest::Error> = service + .call(Request::new( + reqwest::Method::GET, + Url::parse("https://graph.microsoft.com/v1.0/users/id/drive").unwrap(), + )) + .await; + let response = res.unwrap(); + assert_eq!(response.status().as_u16(), 401); + } +} + +/* + +pub struct ThrottleRetry<S> { + inner: S +} + +impl<S> ThrottleRetry<S> { + fn new(inner: S) -> Self { + ThrottleRetry { inner } + } +} + +impl<S, Request> Service<Request> for ThrottleRetry<S> + where + S: Service<Request>, + S::Error: Into<BoxError> +{ + type Response = S::Response; + type Error = BoxError; + type Future = RetryFuture<S::Future>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: Request) -> Self::Future { + + let response_future = self.inner.call(req); + let sleep = tokio::time::sleep(self.timeout); + + RetryFuture { + response_future, + sleep, + } + } +} + + +#[pin_project] +struct RetryFuture<F> { + #[pin] + response_future: F, + #[pin] + sleep: Sleep, +} + +impl<F, Error> Future for RetryFuture<F> + where + F: Future<Output = Result<reqwest::Response, Error>>, + Error: Into<BoxError>, +{ + type Output = Result<reqwest::Response, BoxError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.project(); + + match this.response_future.poll(cx) { + Poll::Ready(result) => { + let result: Result<reqwest::Response, BoxError> = result.map_err(Into::into); + + match result { + Ok(response) => { + let status = response.status(); + if status.is_success() { + return Poll::Ready(Ok(response)); + } + + // 429 Too Many Requests + // Wait on Retry-After Header + if status.as_u16().eq(&429) { + + } + + + + + return Poll::Ready(Ok(response)) + }, + Err(err) => return Poll::Ready(Err(err)) + } + } + Poll::Pending => {} + } + + Poll::Pending + } +} + + +impl<S, Request> Service<Request> for ThrottleRetry<S> +where + S: Service<Request>, + S::Error: Into<BoxError> +{ + type Response = S::Response; + type Error = BoxError; + type Future = Response; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + self.inner.poll_ready(cx).map_err(Into::into) + } + + fn call(&mut self, req: Request) -> Self::Future { + let response_future = self.inner.call(request); + } +} + + */ diff --git a/graph-sdk-abstractions/src/lib.rs b/graph-sdk-abstractions/src/lib.rs new file mode 100644 index 00000000..0f5332aa --- /dev/null +++ b/graph-sdk-abstractions/src/lib.rs @@ -0,0 +1,2 @@ +pub mod backing_store; +pub mod http; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 38b49520..f94cd85d 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - AccessToken, ClientSecretCredential, ResourceOwnerPasswordCredential, - TokenRequest, + AccessToken, ClientSecretCredential, ResourceOwnerPasswordCredential, TokenCredential, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; From 5568eb462ee319bb84b1e63d7fde80b62166b1d0 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 2 Aug 2023 05:50:20 -0400 Subject: [PATCH 025/118] Add ApplicationOptions and builders for public and confidential applications --- examples/oauth/auth_code_grant.rs | 4 +- examples/oauth/auth_code_grant_pkce.rs | 4 +- examples/oauth/client_credentials.rs | 4 +- examples/oauth/device_code.rs | 4 +- examples/oauth/enable_pii_logging.rs | 4 +- examples/oauth/is_access_token_expired.rs | 6 +- examples/oauth/main.rs | 6 +- examples/oauth/open_id_connect.rs | 4 +- examples/oauth_certificate/main.rs | 4 +- graph-oauth/Cargo.toml | 2 +- graph-oauth/src/access_token.rs | 126 ++++++------ graph-oauth/src/auth.rs | 38 ++-- .../src/identity/application_options.rs | 81 ++++++++ graph-oauth/src/identity/authority.rs | 88 +++++++-- .../src/identity/authorization_serializer.rs | 6 +- .../in_memory_credential_store.rs | 4 +- .../src/identity/credential_store/mod.rs | 4 +- .../credentials/application_builder.rs | 187 ++++++++++++++++++ .../auth_code_authorization_url_parameters.rs | 12 +- ...thorization_code_certificate_credential.rs | 4 +- .../authorization_code_credential.rs | 4 +- .../credentials/client_application.rs | 6 +- .../client_certificate_credential.rs | 4 +- .../client_credentials_authorization_url.rs | 6 +- .../credentials/client_secret_credential.rs | 4 +- .../confidential_client_application.rs | 10 +- .../credentials/device_code_credential.rs | 4 +- .../credentials/environment_credential.rs | 4 +- .../credentials/implicit_credential.rs | 6 +- .../legacy/code_flow_credential.rs | 8 +- graph-oauth/src/identity/credentials/mod.rs | 1 + .../credentials/open_id_authorization_url.rs | 10 +- .../credentials/open_id_credential.rs | 4 +- .../credentials/public_client_application.rs | 4 +- .../resource_owner_password_credential.rs | 4 +- .../identity/credentials/token_credential.rs | 4 +- .../credentials/token_credential_options.rs | 4 +- .../src/identity/credentials/token_request.rs | 2 +- graph-oauth/src/identity/mod.rs | 1 + graph-oauth/src/lib.rs | 2 +- graph-sdk-abstractions/src/policy/mod.rs | 0 .../src/policy/retry_after_policy.rs | 0 src/client/graph.rs | 6 +- test-tools/src/oauth_request.rs | 20 +- tests/access_token_tests.rs | 10 +- tests/grants_authorization_code.rs | 4 +- tests/grants_code_flow.rs | 8 +- 47 files changed, 531 insertions(+), 201 deletions(-) create mode 100644 graph-oauth/src/identity/application_options.rs create mode 100644 graph-oauth/src/identity/credentials/application_builder.rs create mode 100644 graph-sdk-abstractions/src/policy/mod.rs create mode 100644 graph-sdk-abstractions/src/policy/retry_after_policy.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 72552293..1a395620 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::oauth::{ - AccessToken, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, + MsalTokenResponse, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, TokenCredential, TokenRequest, }; use graph_rs_sdk::*; @@ -84,7 +84,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let mut access_token: AccessToken = response.json().await.unwrap(); + let mut access_token: MsalTokenResponse = response.json().await.unwrap(); // Enables the printing of the bearer, refresh, and id token. access_token.enable_pii_logging(true); diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 8be51798..f63dc474 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ - AccessToken, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, + MsalTokenResponse, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, ProofKeyForCodeExchange, TokenCredential, TokenRequest, }; use lazy_static::lazy_static; @@ -78,7 +78,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let access_token: AccessToken = response.json().await.unwrap(); + let access_token: MsalTokenResponse = response.json().await.unwrap(); // If all went well here we can print out the OAuth config with the Access Token. println!("AccessToken: {:#?}", access_token.access_token); diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index 751e25a3..d706f94b 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -10,7 +10,7 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - AccessToken, ClientSecretCredential, ConfidentialClientApplication, TokenCredential, + MsalTokenResponse, ClientSecretCredential, ConfidentialClientApplication, TokenCredential, TokenRequest, }; @@ -33,5 +33,5 @@ pub async fn get_token_silent() { .unwrap(); println!("{response:#?}"); - let body: AccessToken = response.json().await.unwrap(); + let body: MsalTokenResponse = response.json().await.unwrap(); } diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 262c5632..5cae5262 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,5 +1,5 @@ use graph_oauth::identity::{DeviceCodeCredential, TokenCredential}; -use graph_rs_sdk::oauth::{AccessToken, OAuthSerializer}; +use graph_rs_sdk::oauth::{MsalTokenResponse, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; @@ -131,7 +131,7 @@ pub async fn device_code() -> GraphResult<()> { // Poll for the response to the token endpoint. This will go through once // the user has entered the code and signed in. let access_token_json = poll_for_access_token(device_code, interval, message).await?; - let access_token: AccessToken = serde_json::from_value(access_token_json)?; + let access_token: MsalTokenResponse = serde_json::from_value(access_token_json)?; println!("{access_token:#?}"); // Get a refresh token. First pass the access token to the oauth instance. diff --git a/examples/oauth/enable_pii_logging.rs b/examples/oauth/enable_pii_logging.rs index ac3cd651..124d9e37 100644 --- a/examples/oauth/enable_pii_logging.rs +++ b/examples/oauth/enable_pii_logging.rs @@ -4,9 +4,9 @@ // You can enable logging of these fields by setting the enable personally // identifiable information field to true called enable_pii. -use graph_rs_sdk::oauth::AccessToken; +use graph_rs_sdk::oauth::MsalTokenResponse; -fn enable_pii_on_access_token(access_token: &mut AccessToken) { +fn enable_pii_on_access_token(access_token: &mut MsalTokenResponse) { access_token.enable_pii_logging(true); println!("{access_token:#?}"); } diff --git a/examples/oauth/is_access_token_expired.rs b/examples/oauth/is_access_token_expired.rs index ad262ec3..cb4fed67 100644 --- a/examples/oauth/is_access_token_expired.rs +++ b/examples/oauth/is_access_token_expired.rs @@ -1,14 +1,14 @@ -use graph_rs_sdk::oauth::AccessToken; +use graph_rs_sdk::oauth::MsalTokenResponse; use std::thread; use std::time::Duration; pub fn is_access_token_expired() { - let mut access_token = AccessToken::default(); + let mut access_token = MsalTokenResponse::default(); access_token.set_expires_in(1); thread::sleep(Duration::from_secs(3)); assert!(access_token.is_expired()); - let mut access_token = AccessToken::default(); + let mut access_token = MsalTokenResponse::default(); access_token.set_expires_in(10); thread::sleep(Duration::from_secs(4)); assert!(!access_token.is_expired()); diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index e6eee006..f923712c 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -29,7 +29,7 @@ mod open_id_connect; mod signing_keys; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, + MsalTokenResponse, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenCredential, TokenRequest, @@ -78,7 +78,7 @@ async fn auth_code_grant(authorization_code: &str) { let response = confidential_client.get_token_async().await.unwrap(); println!("{response:#?}"); - let access_token: AccessToken = response.json().await.unwrap(); + let access_token: MsalTokenResponse = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } @@ -90,6 +90,6 @@ async fn client_credentials() { let response = confidential_client.get_token_async().await.unwrap(); println!("{response:#?}"); - let access_token: AccessToken = response.json().await.unwrap(); + let access_token: MsalTokenResponse = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index da219c21..d4937195 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,6 +1,6 @@ use graph_oauth::identity::{ResponseType, TokenCredential, TokenRequest}; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; -use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuthSerializer}; +use graph_rs_sdk::oauth::{MsalTokenResponse, IdToken, OAuthSerializer}; use url::Url; /// # Example /// ``` @@ -64,7 +64,7 @@ async fn handle_redirect( if let Ok(response) = result { if response.status().is_success() { - let mut access_token: AccessToken = response.json().await.unwrap(); + let mut access_token: MsalTokenResponse = response.json().await.unwrap(); access_token.enable_pii_logging(true); println!("\n{:#?}\n", access_token); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index bb1cc990..8234f649 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,7 +4,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AccessToken, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, + MsalTokenResponse, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, TokenCredential, X509Certificate, X509, }; use std::fs::File; @@ -120,7 +120,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let access_token: AccessToken = response.json().await.unwrap(); + let access_token: MsalTokenResponse = response.json().await.unwrap(); // If all went well here we can print out the Access Token. println!("AccessToken: {:#?}", access_token.access_token); diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index c551d995..43c8fe77 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -30,7 +30,7 @@ serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.24.1", features = ["derive"] } -url = "2" +url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset"] } webbrowser = "0.8.7" wry = "0.28.3" diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index b4c395d9..617d53d7 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -26,6 +26,8 @@ struct PhantomAccessToken { user_id: Option<String>, id_token: Option<String>, state: Option<String>, + correlation_id: Option<String>, + client_info: Option<String>, #[serde(flatten)] additional_fields: HashMap<String, Value>, } @@ -35,10 +37,10 @@ struct PhantomAccessToken { /// Create a new AccessToken. /// # Example /// ``` -/// # use graph_oauth::oauth::AccessToken; -/// let access_token = AccessToken::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); +/// # use graph_oauth::oauth::MsalTokenResponse; +/// let token_response = MsalTokenResponse::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); /// ``` -/// The [AccessToken::jwt] method attempts to parse the access token as a JWT. +/// The [MsalTokenResponse::jwt] method attempts to parse the access token as a JWT. /// Tokens returned for personal microsoft accounts that use legacy MSA /// are encrypted and cannot be parsed. This bearer token may still be /// valid but the jwt() method will return None. @@ -50,14 +52,14 @@ struct PhantomAccessToken { /// /// # Example /// ``` -/// # use graph_oauth::oauth::AccessToken; -/// # let mut access_token = AccessToken::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); +/// # use graph_oauth::oauth::MsalTokenResponse; +/// # let mut token = MsalTokenResponse::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); /// /// // Duration left until expired. -/// println!("{:#?}", access_token.elapsed()); +/// println!("{:#?}", token.elapsed()); /// ``` #[derive(Clone, Eq, PartialEq, Serialize)] -pub struct AccessToken { +pub struct MsalTokenResponse { pub access_token: String, pub token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] @@ -69,6 +71,8 @@ pub struct AccessToken { pub user_id: Option<String>, pub id_token: Option<String>, pub state: Option<String>, + pub correlation_id: Option<String>, + pub client_info: Option<String>, pub timestamp: Option<DateTime<Utc>>, /// Any extra returned fields for AccessToken. #[serde(flatten)] @@ -77,9 +81,9 @@ pub struct AccessToken { log_pii: bool, } -impl AccessToken { - pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> AccessToken { - AccessToken { +impl MsalTokenResponse { + pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> MsalTokenResponse { + MsalTokenResponse { token_type: token_type.into(), ext_expires_in: Some(expires_in.clone()), expires_in: expires_in.clone(), @@ -89,6 +93,8 @@ impl AccessToken { user_id: None, id_token: None, state: None, + correlation_id: None, + client_info: None, timestamp: Some(Utc::now() + Duration::seconds(expires_in)), additional_fields: Default::default(), log_pii: false, @@ -99,12 +105,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_token_type("Bearer"); /// ``` - pub fn set_token_type(&mut self, s: &str) -> &mut AccessToken { + pub fn set_token_type(&mut self, s: &str) -> &mut MsalTokenResponse { self.token_type = s.into(); self } @@ -113,12 +119,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_expires_in(3600); /// ``` - pub fn set_expires_in(&mut self, expires_in: i64) -> &mut AccessToken { + pub fn set_expires_in(&mut self, expires_in: i64) -> &mut MsalTokenResponse { self.expires_in = expires_in; self.timestamp = Some(Utc::now() + Duration::seconds(expires_in)); self @@ -128,12 +134,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_scope("Read Read.Write"); /// ``` - pub fn set_scope(&mut self, s: &str) -> &mut AccessToken { + pub fn set_scope(&mut self, s: &str) -> &mut MsalTokenResponse { self.scope = Some(s.to_string()); self } @@ -142,12 +148,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_bearer_token("ASODFIUJ34KJ;LADSK"); /// ``` - pub fn set_bearer_token(&mut self, s: &str) -> &mut AccessToken { + pub fn set_bearer_token(&mut self, s: &str) -> &mut MsalTokenResponse { self.access_token = s.into(); self } @@ -156,12 +162,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_refresh_token("#ASOD323U5342"); /// ``` - pub fn set_refresh_token(&mut self, s: &str) -> &mut AccessToken { + pub fn set_refresh_token(&mut self, s: &str) -> &mut MsalTokenResponse { self.refresh_token = Some(s.to_string()); self } @@ -170,12 +176,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_user_id("user_id"); /// ``` - pub fn set_user_id(&mut self, s: &str) -> &mut AccessToken { + pub fn set_user_id(&mut self, s: &str) -> &mut MsalTokenResponse { self.user_id = Some(s.to_string()); self } @@ -184,12 +190,12 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{AccessToken, IdToken}; + /// # use graph_oauth::oauth::{MsalTokenResponse, IdToken}; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_id_token("id_token"); /// ``` - pub fn set_id_token(&mut self, s: &str) -> &mut AccessToken { + pub fn set_id_token(&mut self, s: &str) -> &mut MsalTokenResponse { self.id_token = Some(s.to_string()); self } @@ -198,9 +204,9 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{AccessToken, IdToken}; + /// # use graph_oauth::oauth::{MsalTokenResponse, IdToken}; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); /// ``` pub fn with_id_token(&mut self, id_token: IdToken) { @@ -215,20 +221,20 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// # use graph_oauth::oauth::IdToken; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_state("state"); /// ``` - pub fn set_state(&mut self, s: &str) -> &mut AccessToken { + pub fn set_state(&mut self, s: &str) -> &mut MsalTokenResponse { self.state = Some(s.to_string()); self } /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled - /// passing [AccessToken] to logging or print functions will log both the bearer + /// passing [MsalTokenResponse] to logging or print functions will log both the bearer /// access token value, the refresh token value if any, and the id token value. /// By default these do not get logged. pub fn enable_pii_logging(&mut self, log_pii: bool) { @@ -249,15 +255,15 @@ impl AccessToken { /// from when the token was first retrieved. /// /// This will reset the the timestamp from Utc Now + expires_in. This means - /// that if calling [AccessToken::gen_timestamp] will only be reliable if done + /// that if calling [MsalTokenResponse::gen_timestamp] will only be reliable if done /// when the access token is first retrieved. /// /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.expires_in = 86999; /// access_token.gen_timestamp(); /// println!("{:#?}", access_token.timestamp); @@ -269,15 +275,15 @@ impl AccessToken { /// Check whether the access token is expired. Uses the expires_in /// field to check time elapsed since token was first deserialized. - /// This is done using a Utc timestamp set when the [AccessToken] is + /// This is done using a Utc timestamp set when the [MsalTokenResponse] is /// deserialized from the api response /// /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// println!("{:#?}", access_token.is_expired()); /// ``` pub fn is_expired(&self) -> bool { @@ -294,9 +300,9 @@ impl AccessToken { /// /// # Example /// ``` - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// println!("{:#?}", access_token.elapsed()); /// ``` pub fn elapsed(&self) -> Option<HumanTime> { @@ -312,9 +318,9 @@ impl AccessToken { } } -impl Default for AccessToken { +impl Default for MsalTokenResponse { fn default() -> Self { - AccessToken { + MsalTokenResponse { token_type: String::new(), expires_in: 0, ext_expires_in: Some(0), @@ -324,6 +330,8 @@ impl Default for AccessToken { user_id: None, id_token: None, state: None, + correlation_id: None, + client_info: None, timestamp: Some(Utc::now() + Duration::seconds(0)), additional_fields: Default::default(), log_pii: false, @@ -331,7 +339,7 @@ impl Default for AccessToken { } } -impl TryFrom<&str> for AccessToken { +impl TryFrom<&str> for MsalTokenResponse { type Error = GraphFailure; fn try_from(value: &str) -> Result<Self, Self::Error> { @@ -339,35 +347,35 @@ impl TryFrom<&str> for AccessToken { } } -impl TryFrom<reqwest::blocking::RequestBuilder> for AccessToken { +impl TryFrom<reqwest::blocking::RequestBuilder> for MsalTokenResponse { type Error = GraphFailure; fn try_from(value: reqwest::blocking::RequestBuilder) -> Result<Self, Self::Error> { let response = value.send()?; - AccessToken::try_from(response) + MsalTokenResponse::try_from(response) } } -impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for AccessToken { +impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for MsalTokenResponse { type Error = GraphFailure; fn try_from( value: Result<reqwest::blocking::Response, reqwest::Error>, ) -> Result<Self, Self::Error> { let response = value?; - AccessToken::try_from(response) + MsalTokenResponse::try_from(response) } } -impl TryFrom<reqwest::blocking::Response> for AccessToken { +impl TryFrom<reqwest::blocking::Response> for MsalTokenResponse { type Error = GraphFailure; fn try_from(value: reqwest::blocking::Response) -> Result<Self, Self::Error> { - Ok(value.json::<AccessToken>()?) + Ok(value.json::<MsalTokenResponse>()?) } } -impl fmt::Debug for AccessToken { +impl fmt::Debug for MsalTokenResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.log_pii { f.debug_struct("AccessToken") @@ -398,19 +406,19 @@ impl fmt::Debug for AccessToken { } } -impl AsRef<str> for AccessToken { +impl AsRef<str> for MsalTokenResponse { fn as_ref(&self) -> &str { self.access_token.as_str() } } -impl<'de> Deserialize<'de> for AccessToken { +impl<'de> Deserialize<'de> for MsalTokenResponse { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let phantom_access_token: PhantomAccessToken = Deserialize::deserialize(deserializer)?; - Ok(AccessToken { + Ok(MsalTokenResponse { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, expires_in: phantom_access_token.expires_in.clone(), @@ -420,6 +428,8 @@ impl<'de> Deserialize<'de> for AccessToken { user_id: phantom_access_token.user_id, id_token: phantom_access_token.id_token, state: phantom_access_token.state, + correlation_id: phantom_access_token.correlation_id, + client_info: phantom_access_token.client_info, timestamp: Some(Utc::now() + Duration::seconds(phantom_access_token.expires_in)), additional_fields: phantom_access_token.additional_fields, log_pii: false, diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 750885d0..8d312ecc 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,7 +1,7 @@ -use crate::access_token::AccessToken; +use crate::access_token::MsalTokenResponse; use crate::grants::{GrantRequest, GrantType}; use crate::id_token::IdToken; -use crate::identity::{AsQuery, Authority, AzureAuthorityHost, Prompt}; +use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; @@ -143,7 +143,7 @@ impl OAuth2Client { /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct OAuthSerializer { - access_token: Option<AccessToken>, + access_token: Option<MsalTokenResponse>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, } @@ -394,10 +394,10 @@ impl OAuthSerializer { /// ``` pub fn authority( &mut self, - host: &AzureAuthorityHost, + host: &AzureCloudInstance, authority: &Authority, ) -> &mut OAuthSerializer { - if host.eq(&AzureAuthorityHost::OneDriveAndSharePoint) { + if host.eq(&AzureCloudInstance::OneDriveAndSharePoint) { return self.legacy_authority(); } @@ -415,7 +415,7 @@ impl OAuthSerializer { pub fn authority_admin_consent( &mut self, - host: &AzureAuthorityHost, + host: &AzureCloudInstance, authority: &Authority, ) -> &mut OAuthSerializer { let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); @@ -427,9 +427,9 @@ impl OAuthSerializer { } pub fn legacy_authority(&mut self) -> &mut OAuthSerializer { - self.authorization_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); - self.access_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()); - self.refresh_token_url(AzureAuthorityHost::OneDriveAndSharePoint.as_ref()) + self.authorization_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()); + self.access_token_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()); + self.refresh_token_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()) } /// Set the redirect url of a request @@ -908,12 +908,12 @@ impl OAuthSerializer { /// # Example /// ``` /// use graph_oauth::oauth::OAuthSerializer; - /// use graph_oauth::oauth::AccessToken; + /// use graph_oauth::oauth::MsalTokenResponse; /// let mut oauth = OAuthSerializer::new(); - /// let access_token = AccessToken::default(); + /// let access_token = MsalTokenResponse::default(); /// oauth.access_token(access_token); /// ``` - pub fn access_token(&mut self, ac: AccessToken) { + pub fn access_token(&mut self, ac: MsalTokenResponse) { if let Some(refresh_token) = ac.refresh_token.as_ref() { self.refresh_token(refresh_token.as_str()); } @@ -925,14 +925,14 @@ impl OAuthSerializer { /// # Example /// ``` /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::AccessToken; - /// # let access_token = AccessToken::default(); + /// # use graph_oauth::oauth::MsalTokenResponse; + /// # let access_token = MsalTokenResponse::default(); /// # let mut oauth = OAuthSerializer::new(); /// # oauth.access_token(access_token); /// let access_token = oauth.get_access_token().unwrap(); /// println!("{:#?}", access_token); /// ``` - pub fn get_access_token(&self) -> Option<AccessToken> { + pub fn get_access_token(&self) -> Option<MsalTokenResponse> { self.access_token.clone() } @@ -943,9 +943,9 @@ impl OAuthSerializer { /// # Example /// ``` /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::AccessToken; + /// # use graph_oauth::oauth::MsalTokenResponse; /// # let mut oauth = OAuthSerializer::new(); - /// let mut access_token = AccessToken::default(); + /// let mut access_token = MsalTokenResponse::default(); /// access_token.set_refresh_token("refresh_token"); /// oauth.access_token(access_token); /// @@ -1730,7 +1730,7 @@ pub struct AccessTokenRequest { impl AccessTokenRequest { /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [AccessToken] + /// should be an access token which you can convert to [MsalTokenResponse] /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example @@ -1809,7 +1809,7 @@ pub struct AsyncAccessTokenRequest { impl AsyncAccessTokenRequest { /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [AccessToken] + /// should be an access token which you can convert to [MsalTokenResponse] /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs new file mode 100644 index 00000000..229c1d67 --- /dev/null +++ b/graph-oauth/src/identity/application_options.rs @@ -0,0 +1,81 @@ +/* + /// <summary> + /// Client ID (also known as App ID) of the application as registered in the + /// application registration portal (https://aka.ms/msal-net-register-app) + /// </summary> + public string ClientId { get; set; } + + /// <summary> + /// Tenant from which the application will allow users to sign it. This can be: + /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). + /// This property is mutually exclusive with <see cref="AadAuthorityAudience"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + /// <remarks>The name of the property was chosen to ensure compatibility with AzureAdOptions + /// in ASP.NET Core configuration files (even the semantics would be tenant)</remarks> + public string TenantId { get; set; } + + /// <summary> + /// Sign-in audience. This property is mutually exclusive with TenantId. If both + /// are provided, an exception will be thrown. + /// </summary> + public AadAuthorityAudience AadAuthorityAudience { get; set; } = AadAuthorityAudience.None; + + /// <summary> + /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). + /// The name was chosen to ensure compatibility with AzureAdOptions in ASP.NET Core. + /// This property is mutually exclusive with <see cref="AzureCloudInstance"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + public string Instance { get; set; } + + /// <summary> + /// Specific instance in the case of Azure Active Directory. + /// It allows users to use the enum instead of the explicit URL. + /// This property is mutually exclusive with <see cref="Instance"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + public AzureCloudInstance AzureCloudInstance { get; set; } = AzureCloudInstance.None; + + /// <summary> + /// This redirect URI needs to be registered in the app registration. See https://aka.ms/msal-net-register-app for + /// details on which redirect URIs are defined by default by MSAL.NET and how to register them. + /// Also use: <see cref="PublicClientApplicationBuilder.WithDefaultRedirectUri"/> which provides + /// a good default for public client applications for all platforms. + /// + /// For web apps and web APIs, the redirect URI is computed from the URL where the application is running + /// (for instance, <c>baseUrl//signin-oidc</c> for ASP.NET Core web apps). + /// + /// For daemon applications (confidential client applications using only the Client Credential flow + /// that is calling <c>AcquireTokenForClient</c>), no reply URI is needed. + /// </summary> + /// <remarks>This is especially important when you deploy an application that you have initially tested locally; + /// you then need to add the reply URL of the deployed application in the application registration portal + /// </remarks> + public string RedirectUri { get; set; } + */ + +use url::Url; +use crate::identity::AadAuthorityAudience; +use crate::oauth::AzureCloudInstance; + +/// Application Options typically stored as JSON file in .net applications. +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub struct ApplicationOptions { + /// Client ID (also known as App ID) of the application as registered in the + /// application registration portal (https://aka.ms/msal-net-register-app) + /// Required parameter for ApplicationOptions. + #[serde(alias = "clientId", alias = "ClientId")] + pub client_id: String, + /// Tenant from which the application will allow users to sign it. This can be: + /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). + /// This property is mutually exclusive with [AadAuthorityAudience]. If both + /// are provided, an error will be thrown. + #[serde(alias = "tenantId", alias = "TenantId")] + pub tenant_id: Option<String>, + pub aad_authority_audience: Option<AadAuthorityAudience>, + #[serde(alias = "instance", alias = "Instance")] + pub instance: Option<Url>, + pub azure_cloud_instance: Option<AzureCloudInstance>, + pub redirect_uri: Option<Url> +} diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 4c2aa2b7..71ad5538 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -1,10 +1,9 @@ use url::Url; /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). -/// Authentication libraries from Microsoft (this is not one) call this the -/// AzureCloudInstance enum or the Instance url string. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum AzureAuthorityHost { +/// Maps to the instance url string. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum AzureCloudInstance { // Custom Value communicating that the AzureCloudInstance. //Custom(String), /// Microsoft Azure public cloud. Maps to https://login.microsoftonline.com @@ -16,54 +15,92 @@ pub enum AzureAuthorityHost { AzureGermany, /// US Government cloud. Maps to https://login.microsoftonline.us AzureUsGovernment, - + /// Legacy OneDrive and SharePoint. Maps to "https://login.live.com/oauth20_desktop.srf" OneDriveAndSharePoint, } -impl AsRef<str> for AzureAuthorityHost { +impl AsRef<str> for AzureCloudInstance { fn as_ref(&self) -> &str { match self { - AzureAuthorityHost::AzurePublic => "https://login.microsoftonline.com", - AzureAuthorityHost::AzureChina => "https://login.chinacloudapi.cn", - AzureAuthorityHost::AzureGermany => "https://login.microsoftonline.de", - AzureAuthorityHost::AzureUsGovernment => "https://login.microsoftonline.us", - AzureAuthorityHost::OneDriveAndSharePoint => { + AzureCloudInstance::AzurePublic => "https://login.microsoftonline.com", + AzureCloudInstance::AzureChina => "https://login.chinacloudapi.cn", + AzureCloudInstance::AzureGermany => "https://login.microsoftonline.de", + AzureCloudInstance::AzureUsGovernment => "https://login.microsoftonline.us", + AzureCloudInstance::OneDriveAndSharePoint => { "https://login.live.com/oauth20_desktop.srf" } } } } -impl TryFrom<AzureAuthorityHost> for Url { +impl TryFrom<AzureCloudInstance> for Url { type Error = url::ParseError; - fn try_from(azure_cloud_instance: AzureAuthorityHost) -> Result<Self, Self::Error> { + fn try_from(azure_cloud_instance: AzureCloudInstance) -> Result<Self, Self::Error> { Url::parse(azure_cloud_instance.as_ref()) } } -impl AzureAuthorityHost { +impl AzureCloudInstance { pub fn default_microsoft_graph_scope(&self) -> &'static str { "https://graph.microsoft.com/.default" } pub fn default_managed_identity_scope(&self) -> &'static str { match self { - AzureAuthorityHost::AzurePublic => "https://management.azure.com//.default", - AzureAuthorityHost::AzureChina => "https://management.chinacloudapi.cn/.default", - AzureAuthorityHost::AzureGermany => "https://management.microsoftazure.de/.default", - AzureAuthorityHost::AzureUsGovernment => { + AzureCloudInstance::AzurePublic => "https://management.azure.com//.default", + AzureCloudInstance::AzureChina => "https://management.chinacloudapi.cn/.default", + AzureCloudInstance::AzureGermany => "https://management.microsoftazure.de/.default", + AzureCloudInstance::AzureUsGovernment => { "https://management.usgovcloudapi.net/.default" } - AzureAuthorityHost::OneDriveAndSharePoint => "", + AzureCloudInstance::OneDriveAndSharePoint => "", } } } +/// Specifies which Microsoft accounts can be used for sign-in with a given application. +/// See https://aka.ms/msal-net-application-configuration +/// +/// [AadAuthorityAudience] uses the application names selected in the Azure Portal and +/// maps to [Authority] +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub enum AadAuthorityAudience { + /// The sign-in audience was not specified + #[default] + None, + + /// Users with a Microsoft work or school account in my organization’s Azure AD tenant (i.e. single tenant). + /// Maps to https://[AzureCloudInstance]/[AadAuthorityAudience::AzureAdMyOrg(tenant_id)] + /// or https://[instance]/[tenant_id] + /// + /// # Using Tenant Id + /// ```rust + /// # use graph_oauth::oauth::AadAuthorityAudience; + /// let authority_audience = AadAuthorityAudience::AzureAdMyOrg("tenant_id".into()); + /// ``` + AzureAdMyOrg(String), + + /// Users with a personal Microsoft account, or a work or school account in any organization’s Azure AD tenant + /// Maps to https://[AzureCloudInstance]/common/ or https://[instance]/[common]/ + AzureAdAndPersonalMicrosoftAccount, + + /// Users with a Microsoft work or school account in any organization’s Azure AD tenant (i.e. multi-tenant). + /// Maps to https://[AzureCloudInstance]/organizations/ or https://[instance]/organizations/ + AzureAdMultipleOrgs, + + /// Users with a personal Microsoft account. Maps to https://[AzureCloudInstance]/consumers/ + /// or https://[instance]/consumers/ + PersonalMicrosoftAccount +} + +/// Specifies which Microsoft accounts can be used for sign-in with a given application. +/// See https://aka.ms/msal-net-application-configuration #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Authority { /// Users with both a personal Microsoft account and a work or school account /// from Azure AD can sign in to the application. + /// /// Maps to https://[AzureCloudInstance]/common/ /// /// [Authority::AzureActiveDirectory] is the same as [Authority::Common]. /// [Authority::Common] is a convenience enum variant that may be more @@ -73,6 +110,7 @@ pub enum Authority { AzureDirectoryFederatedServices, /// Users with both a personal Microsoft account and a work or school account /// from Azure AD can sign in to the application. + /// Maps to https://[instance]/common/ /// /// [Authority::Common] is the same as [Authority::AzureActiveDirectory]. /// @@ -102,6 +140,18 @@ impl Authority { } } +impl From<AadAuthorityAudience> for Authority { + fn from(value: AadAuthorityAudience) -> Self { + match value { + AadAuthorityAudience::None => Authority::Common, + AadAuthorityAudience::AzureAdMyOrg(tenant_id) => Authority::TenantId(tenant_id), + AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount => Authority::Common, + AadAuthorityAudience::AzureAdMultipleOrgs => Authority::Organizations, + AadAuthorityAudience::PersonalMicrosoftAccount => Authority::Consumers, + } + } +} + impl AsRef<str> for Authority { fn as_ref(&self) -> &str { match self { diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index 2de3849c..dab2d99e 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -1,10 +1,10 @@ -use crate::identity::AzureAuthorityHost; +use crate::identity::AzureCloudInstance; use graph_error::AuthorizationResult; use std::collections::HashMap; use url::Url; pub trait AuthorizationSerializer { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn basic_auth(&self) -> Option<(String, String)> { None @@ -16,6 +16,6 @@ pub trait AuthorizationUrl { fn authorization_url(&self) -> AuthorizationResult<Url>; fn authorization_url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url>; } diff --git a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs index df084ea7..2812d04c 100644 --- a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs +++ b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs @@ -1,4 +1,4 @@ -use crate::access_token::AccessToken; +use crate::access_token::MsalTokenResponse; use crate::identity::{CredentialStore, CredentialStoreType, TokenCacheProviderType}; use std::collections::BTreeMap; @@ -28,7 +28,7 @@ impl InMemoryCredentialStore { pub fn from_access_token<T: AsRef<str>>( client_id: T, - access_token: AccessToken, + access_token: MsalTokenResponse, ) -> InMemoryCredentialStore { let mut credentials = BTreeMap::new(); credentials.insert( diff --git a/graph-oauth/src/identity/credential_store/mod.rs b/graph-oauth/src/identity/credential_store/mod.rs index ec92084c..f169648b 100644 --- a/graph-oauth/src/identity/credential_store/mod.rs +++ b/graph-oauth/src/identity/credential_store/mod.rs @@ -4,13 +4,13 @@ mod token_cache_providers; pub use in_memory_credential_store::*; pub use token_cache_providers::*; -use crate::oauth::AccessToken; +use crate::oauth::MsalTokenResponse; #[derive(Debug, Clone, Eq, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum CredentialStoreType { Bearer(String), - AccessToken(AccessToken), + AccessToken(MsalTokenResponse), UnInitialized, } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs new file mode 100644 index 00000000..fd03bdff --- /dev/null +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; +use anyhow::{anyhow, ensure}; +use reqwest::header::HeaderMap; +use url::Url; +#[cfg(feature = "openssl")] +use crate::identity::X509Certificate; +use crate::identity::{Authority}; +use crate::identity::application_options::ApplicationOptions; +use crate::oauth::{AzureCloudInstance, ConfidentialClientApplication}; + +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum AuthorityHost { + /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). + /// Maps to the instance url string. + AzureCloudInstance(AzureCloudInstance), + Uri(Url) +} + +impl From<AzureCloudInstance> for AuthorityHost { + fn from(value: AzureCloudInstance) -> Self { + AuthorityHost::AzureCloudInstance(value) + } +} + +impl From<Url> for AuthorityHost { + fn from(value: Url) -> Self { + AuthorityHost::Uri(value) + } +} + +impl Default for AuthorityHost { + fn default() -> Self { + AuthorityHost::AzureCloudInstance(AzureCloudInstance::default()) + } +} + +pub enum ClientCredentialParameter { + #[cfg(feature = "openssl")] + CertificateClientCredential(X509Certificate), + SecretStringClientCredential(String), + SignedAssertionClientCredential(String), +} + +pub struct ConfidentialClientApplicationBuilder { + client_id: String, + tenant_id: Option<String>, + authority: Authority, + authority_url: AuthorityHost, + redirect_uri: Option<Url>, + default_redirect_uri: bool, + client_credential_parameter: Option<ClientCredentialParameter>, + extra_query_parameters: HashMap<String, String>, + extra_header_parameters: HeaderMap, +} + +impl ConfidentialClientApplicationBuilder { + pub fn create(client_id: &str) -> ConfidentialClientApplicationBuilder { + ConfidentialClientApplicationBuilder { + client_id: client_id.to_owned(), + tenant_id: None, + authority: Default::default(), + authority_url: Default::default(), + redirect_uri: None, + default_redirect_uri: false, + client_credential_parameter: None, + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + } + } + + pub fn create_with_application_options(application_options: ApplicationOptions) -> anyhow::Result<ConfidentialClientApplicationBuilder> { + ConfidentialClientApplicationBuilder::try_from(application_options) + } + + pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + self.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + self.tenant_id = Some(tenant_id.as_ref().to_owned()); + self.authority = Authority::TenantId(tenant_id.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<AuthorityHost>, U: Into<Authority>>( + &mut self, + authority_host: T, + authority: U, + ) -> &mut Self { + self.authority_url = authority_host.into(); + self.authority = authority.into(); + self + } + + /// Adds a known Azure AD authority to the application to sign-in users specifying + /// the full authority Uri. See https://aka.ms/msal-net-application-configuration. + pub fn with_authority_uri(&mut self, authority_uri: Url) -> &mut Self { + self.authority_url = AuthorityHost::Uri(authority_uri); + self + } + + pub fn with_azure_cloud_instance(&mut self, azure_cloud_instance: AzureCloudInstance) -> &mut Self { + self.authority_url = AuthorityHost::AzureCloudInstance(azure_cloud_instance); + self + } + + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.redirect_uri = Some(redirect_uri); + self.default_redirect_uri = false; + self + } + + pub fn with_default_redirect_uri(&mut self) -> &mut Self { + self.default_redirect_uri = true; + self + } + + #[cfg(feature = "openssl")] + pub fn with_certificate(&mut self, certificate: X509Certificate) -> &mut self { + self.client_credential_parameter = Some(ClientCredentialParameter::CertificateClientCredential(certificate)); + self + } + + pub fn with_client_secret(&mut self, client_secret: impl AsRef<str>) -> &mut Self { + self.client_credential_parameter = Some(ClientCredentialParameter::SecretStringClientCredential(client_secret.as_ref().to_owned())); + self + } + + pub fn with_signed_assertion(&mut self, signed_assertion: impl AsRef<str>) -> &mut Self { + self.client_credential_parameter = Some(ClientCredentialParameter::SignedAssertionClientCredential(signed_assertion.as_ref().to_owned())); + self + } + + pub fn with_extra_query_parameters(&mut self, query_parameters: HashMap<String, String>) -> &mut Self { + self.extra_query_parameters = query_parameters; + self + } + + pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { + self.extra_header_parameters = header_parameters; + self + } +} + +impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { + type Error = anyhow::Error; + + fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { + anyhow::ensure!(value.client_id.is_empty(), "Client id cannot be empty"); + anyhow::ensure!(!(value.instance.is_some() && value.azure_cloud_instance.is_some()), "Instance and AzureCloudInstance cannot both be set"); + anyhow::ensure!(!(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), "TenantId and AadAuthorityAudience cannot both be set"); + let default_redirect_uri = value.redirect_uri.is_none(); + + Ok(ConfidentialClientApplicationBuilder { + client_id: value.client_id, + tenant_id: value.tenant_id, + authority: value.aad_authority_audience.map(|aud| Authority::from(aud)) + .unwrap_or_default(), + authority_url: value.azure_cloud_instance.map(|aci| AuthorityHost::AzureCloudInstance(aci)) + .unwrap_or_default(), + redirect_uri: value.redirect_uri, + default_redirect_uri, + client_credential_parameter: None, + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[should_panic] + fn error_result_on_instance_and_aci() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }).unwrap(); + } +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index b3f1f107..3be0bb8b 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::auth_response_query::AuthQueryResponse; use crate::identity::{ - Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, + Authority, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, ResponseMode, }; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; use crate::web::{InteractiveAuthenticator, InteractiveWebViewOptions}; @@ -81,12 +81,12 @@ impl AuthCodeAuthorizationUrlParameters { } pub fn url(&self) -> AuthorizationResult<Url> { - self.url_with_host(&AzureAuthorityHost::default()) + self.url_with_host(&AzureCloudInstance::default()) } pub fn url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { self.authorization_url_with_host(azure_authority_host) } @@ -187,12 +187,12 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { } fn authorization_url(&self) -> AuthorizationResult<Url> { - self.authorization_url_with_host(&AzureAuthorityHost::default()) + self.authorization_url_with_host(&AzureCloudInstance::default()) } fn authorization_url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); @@ -577,7 +577,7 @@ mod test { .with_scope(["read", "write"]) .build(); - let url_result = authorizer.url_with_host(&AzureAuthorityHost::AzureGermany); + let url_result = authorizer.url_with_host(&AzureCloudInstance::AzureGermany); assert!(url_result.is_ok()); } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 882db6bb..6d14a5c1 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AuthorizationSerializer, AzureAuthorityHost, TokenCredential, TokenCredentialOptions, + AuthorizationSerializer, AzureCloudInstance, TokenCredential, TokenCredentialOptions, TokenRequest, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; @@ -104,7 +104,7 @@ impl AuthorizationCodeCertificateCredential { #[async_trait] impl TokenCredential for AuthorizationCodeCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 76676f6e..6c18d873 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - AuthCodeAuthorizationUrlParameters, Authority, AuthorizationSerializer, AzureAuthorityHost, + AuthCodeAuthorizationUrlParameters, Authority, AuthorizationSerializer, AzureCloudInstance, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; @@ -102,7 +102,7 @@ impl AuthorizationCodeCredential { #[async_trait] impl TokenCredential for AuthorizationCodeCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs index 4862f552..6d95ed53 100644 --- a/graph-oauth/src/identity/credentials/client_application.rs +++ b/graph-oauth/src/identity/credentials/client_application.rs @@ -1,5 +1,5 @@ use crate::identity::{CredentialStoreType, TokenCredential}; -use crate::oauth::AccessToken; +use crate::oauth::MsalTokenResponse; use async_trait::async_trait; #[async_trait] @@ -17,7 +17,7 @@ pub trait ClientApplication: TokenCredential { let response = self.get_token()?; let token_value: serde_json::Value = response.json()?; let bearer = token_value.to_string(); - let access_token_result: serde_json::Result<AccessToken> = + let access_token_result: serde_json::Result<MsalTokenResponse> = serde_json::from_value(token_value); match access_token_result { Ok(access_token) => { @@ -43,7 +43,7 @@ pub trait ClientApplication: TokenCredential { let response = self.get_token_async().await?; let token_value: serde_json::Value = response.json().await?; let bearer = token_value.to_string(); - let access_token_result: serde_json::Result<AccessToken> = + let access_token_result: serde_json::Result<MsalTokenResponse> = serde_json::from_value(token_value); match access_token_result { Ok(access_token) => { diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 176da938..3c03abb3 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, + Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; @@ -69,7 +69,7 @@ impl ClientCertificateCredential { #[async_trait] impl TokenCredential for ClientCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 12f1250e..66fe8e7a 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureAuthorityHost}; +use crate::identity::{Authority, AzureCloudInstance}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -28,12 +28,12 @@ impl ClientCredentialsAuthorizationUrl { } pub fn url(&self) -> AuthorizationResult<Url> { - self.url_with_host(&AzureAuthorityHost::AzurePublic) + self.url_with_host(&AzureCloudInstance::AzurePublic) } pub fn url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); if self.client_id.trim().is_empty() { diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 57925be1..663dc666 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, + Authority, AuthorizationSerializer, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, TokenCredential, TokenRequest, }; use crate::oauth::TokenCredentialOptions; @@ -85,7 +85,7 @@ impl ClientSecretCredential { #[async_trait] impl TokenCredential for ClientSecretCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 22a17376..7c35b0c0 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,6 +1,6 @@ use crate::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - AzureAuthorityHost, ClientApplication, ClientCertificateCredential, ClientSecretCredential, + AzureCloudInstance, ClientApplication, ClientCertificateCredential, ClientSecretCredential, CredentialStore, CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, TokenCacheProviderType, TokenCredential, TokenCredentialOptions, TokenRequest, }; @@ -40,7 +40,7 @@ impl ConfidentialClientApplication { #[async_trait] impl TokenCredential for ConfidentialClientApplication { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } @@ -223,7 +223,7 @@ impl From<OpenIdCredential> for ConfidentialClientApplication { #[cfg(test)] mod test { use super::*; - use crate::identity::{Authority, AzureAuthorityHost}; + use crate::identity::{Authority, AzureCloudInstance}; #[test] fn confidential_client_new() { @@ -241,7 +241,7 @@ mod test { .unwrap(); let credential_uri = confidential_client .credential - .uri(&AzureAuthorityHost::AzurePublic) + .uri(&AzureCloudInstance::AzurePublic) .unwrap(); assert_eq!( @@ -266,7 +266,7 @@ mod test { .unwrap(); let credential_uri = confidential_client .credential - .uri(&AzureAuthorityHost::AzurePublic) + .uri(&AzureCloudInstance::AzurePublic) .unwrap(); assert_eq!( diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 10a687aa..84d8f819 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, + Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, TokenCredentialOptions, TokenRequest, }; use crate::oauth::{DeviceCode, PublicClientApplication}; @@ -159,7 +159,7 @@ impl DeviceCodeCredential { } impl TokenCredential for DeviceCodeCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index daba2530..0312a93b 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -1,5 +1,5 @@ use crate::identity::{ - AuthorizationSerializer, AzureAuthorityHost, ClientSecretCredential, TokenCredential, + AuthorizationSerializer, AzureCloudInstance, ClientSecretCredential, TokenCredential, }; use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, @@ -133,7 +133,7 @@ impl EnvironmentCredential { } impl AuthorizationSerializer for EnvironmentCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 7bbe13f7..e3ad10d5 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureAuthorityHost, Crypto, Prompt, ResponseMode, ResponseType}; +use crate::identity::{Authority, AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; use url::form_urlencoded::Serializer; @@ -118,12 +118,12 @@ impl ImplicitCredential { } pub fn url(&self) -> AuthorizationResult<Url> { - self.url_with_host(&AzureAuthorityHost::default()) + self.url_with_host(&AzureCloudInstance::default()) } pub fn url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs index c004a555..b536235c 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs @@ -1,5 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AuthorizationSerializer, AzureAuthorityHost}; +use crate::identity::{Authority, AuthorizationSerializer, AzureCloudInstance}; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; @@ -59,11 +59,11 @@ impl CodeFlowCredential { } impl AuthorizationSerializer for CodeFlowCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { - if azure_authority_host.ne(&AzureAuthorityHost::OneDriveAndSharePoint) { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + if azure_authority_host.ne(&AzureCloudInstance::OneDriveAndSharePoint) { return AuthorizationFailure::msg_result( "uri", - "Code flow can only be used with AzureAuthorityHost::OneDriveAndSharePoint", + "Code flow can only be used with AzureCloudInstance::OneDriveAndSharePoint", ); } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index dbf29b68..9c15685e 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -12,6 +12,7 @@ mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; +mod application_builder; mod crypto; mod device_code_credential; mod display; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 9c2d2863..39540aa8 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - AsQuery, Authority, AuthorizationUrl, AzureAuthorityHost, Crypto, Prompt, ResponseMode, + AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType, }; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; @@ -145,12 +145,12 @@ impl OpenIdAuthorizationUrl { } pub fn url(&self) -> AuthorizationResult<Url> { - self.url_with_host(&AzureAuthorityHost::default()) + self.url_with_host(&AzureCloudInstance::default()) } pub fn url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { self.authorization_url_with_host(azure_authority_host) } @@ -178,12 +178,12 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } fn authorization_url(&self) -> AuthorizationResult<Url> { - self.authorization_url_with_host(&AzureAuthorityHost::default()) + self.authorization_url_with_host(&AzureCloudInstance::default()) } fn authorization_url_with_host( &self, - azure_authority_host: &AzureAuthorityHost, + azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 1fc24246..b8acda9e 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, OpenIdAuthorizationUrl, + Authority, AuthorizationSerializer, AzureCloudInstance, OpenIdAuthorizationUrl, ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, }; use crate::oauth::OpenIdAuthorizationUrlBuilder; @@ -97,7 +97,7 @@ impl OpenIdCredential { #[async_trait] impl TokenCredential for OpenIdCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 9f74648c..289987e0 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,5 +1,5 @@ use crate::identity::{ - AuthorizationSerializer, AzureAuthorityHost, DeviceCodeCredential, + AuthorizationSerializer, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; @@ -37,7 +37,7 @@ impl PublicClientApplication { #[async_trait] impl TokenCredential for PublicClientApplication { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index f2cf88c3..a9c5d186 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureAuthorityHost, TokenCredential, + Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; @@ -76,7 +76,7 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredential for ResourceOwnerPasswordCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer .authority(azure_authority_host, &self.authority); diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 3bfe4deb..702d27ae 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -1,5 +1,5 @@ use crate::identity::{ - AuthorizationSerializer, AzureAuthorityHost, TokenCredentialOptions, TokenRequest, + AuthorizationSerializer, AzureCloudInstance, TokenCredentialOptions, TokenRequest, }; use async_trait::async_trait; use graph_error::AuthorizationResult; @@ -11,7 +11,7 @@ use url::Url; #[async_trait] pub trait TokenCredential { - fn uri(&mut self, azure_authority_host: &AzureAuthorityHost) -> AuthorizationResult<Url>; + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn client_id(&self) -> &String; fn token_credential_options(&self) -> &TokenCredentialOptions; diff --git a/graph-oauth/src/identity/credentials/token_credential_options.rs b/graph-oauth/src/identity/credentials/token_credential_options.rs index 0ae29b66..103e05f6 100644 --- a/graph-oauth/src/identity/credentials/token_credential_options.rs +++ b/graph-oauth/src/identity/credentials/token_credential_options.rs @@ -1,10 +1,10 @@ -use crate::identity::AzureAuthorityHost; +use crate::identity::AzureCloudInstance; use reqwest::header::HeaderMap; use std::collections::HashMap; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TokenCredentialOptions { - pub(crate) azure_authority_host: AzureAuthorityHost, + pub(crate) azure_authority_host: AzureCloudInstance, pub extra_query_parameters: HashMap<String, String>, diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index fa3b6465..21a4b733 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,4 +1,4 @@ -use crate::identity::{AzureAuthorityHost, TokenCredential}; +use crate::identity::{AzureCloudInstance, TokenCredential}; use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; use graph_error::AuthorizationResult; diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 6b3197be..e974c0c1 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,4 +1,5 @@ mod allowed_host_validator; +mod application_options; mod authority; mod authorization_serializer; mod credential_store; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 2f26c6d0..dcc48fac 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -76,7 +76,7 @@ pub mod identity; pub mod web; pub mod oauth { - pub use crate::access_token::AccessToken; + pub use crate::access_token::MsalTokenResponse; pub use crate::auth::GrantSelector; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; diff --git a/graph-sdk-abstractions/src/policy/mod.rs b/graph-sdk-abstractions/src/policy/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-sdk-abstractions/src/policy/retry_after_policy.rs b/graph-sdk-abstractions/src/policy/retry_after_policy.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/client/graph.rs b/src/client/graph.rs index 9aec4c51..2b1057cd 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AccessToken, AllowedHostValidator, HostValidator, OAuthSerializer}; +use crate::oauth::{MsalTokenResponse, AllowedHostValidator, HostValidator, OAuthSerializer}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -526,8 +526,8 @@ impl From<String> for Graph { } } -impl From<&AccessToken> for Graph { - fn from(token: &AccessToken) -> Self { +impl From<&MsalTokenResponse> for Graph { + fn from(token: &MsalTokenResponse) -> Self { Graph::new(token.access_token.as_str()) } } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index f94cd85d..61303dec 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,7 +3,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - AccessToken, ClientSecretCredential, ResourceOwnerPasswordCredential, TokenCredential, + MsalTokenResponse, ClientSecretCredential, ResourceOwnerPasswordCredential, TokenCredential, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -149,13 +149,13 @@ pub enum OAuthTestClient { } impl OAuthTestClient { - fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, AccessToken)> { + fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, MsalTokenResponse)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); if let Ok(response) = credential.get_token() { - let token: AccessToken = response.json().unwrap(); + let token: MsalTokenResponse = response.json().unwrap(); Some((user_id, token)) } else { None @@ -164,7 +164,7 @@ impl OAuthTestClient { OAuthTestClient::ResourceOwnerPasswordCredentials => { let mut credential = creds.resource_owner_password_credential(); if let Ok(response) = credential.get_token() { - let token: AccessToken = response.json().unwrap(); + let token: MsalTokenResponse = response.json().unwrap(); Some((user_id, token)) } else { None @@ -181,14 +181,14 @@ impl OAuthTestClient { async fn get_access_token_async( &self, creds: OAuthTestCredentials, - ) -> Option<(String, AccessToken)> { + ) -> Option<(String, MsalTokenResponse)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); match credential.get_token_async().await { Ok(response) => { - let token: AccessToken = response.json().await.unwrap(); + let token: MsalTokenResponse = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -198,7 +198,7 @@ impl OAuthTestClient { let mut credential = creds.resource_owner_password_credential(); match credential.get_token_async().await { Ok(response) => { - let token: AccessToken = response.json().await.unwrap(); + let token: MsalTokenResponse = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -208,7 +208,7 @@ impl OAuthTestClient { } } - pub fn request_access_token(&self) -> Option<(String, AccessToken)> { + pub fn request_access_token(&self) -> Option<(String, MsalTokenResponse)> { if Environment::is_local() || Environment::is_travis() { let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); self.get_access_token(map.get(self).unwrap()) @@ -223,7 +223,7 @@ impl OAuthTestClient { } } - pub async fn request_access_token_async(&self) -> Option<(String, AccessToken)> { + pub async fn request_access_token_async(&self) -> Option<(String, MsalTokenResponse)> { if Environment::is_local() || Environment::is_travis() { let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); self.get_access_token_async(map.get(self).unwrap()).await @@ -301,7 +301,7 @@ impl OAuthTestClient { } } - pub fn token(resource_identity: ResourceIdentity) -> Option<AccessToken> { + pub fn token(resource_identity: ResourceIdentity) -> Option<MsalTokenResponse> { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, _credentials) = client.default_client()?; diff --git a/tests/access_token_tests.rs b/tests/access_token_tests.rs index fbf08ed4..009fae81 100644 --- a/tests/access_token_tests.rs +++ b/tests/access_token_tests.rs @@ -1,14 +1,14 @@ -use graph_oauth::oauth::AccessToken; +use graph_oauth::oauth::MsalTokenResponse; use std::thread; use std::time::Duration; #[test] fn is_expired_test() { - let mut access_token = AccessToken::default(); + let mut access_token = MsalTokenResponse::default(); access_token.set_expires_in(1); thread::sleep(Duration::from_secs(3)); assert!(access_token.is_expired()); - let mut access_token = AccessToken::default(); + let mut access_token = MsalTokenResponse::default(); access_token.set_expires_in(10); thread::sleep(Duration::from_secs(4)); assert!(!access_token.is_expired()); @@ -40,6 +40,6 @@ pub const ACCESS_TOKEN_STRING: &str = r#"{ #[test] pub fn test_deserialize() { - let _token: AccessToken = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); - let _token: AccessToken = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); + let _token: MsalTokenResponse = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); + let _token: MsalTokenResponse = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); } diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index 5a47dd70..e663f859 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuthSerializer}; +use graph_rs_sdk::oauth::{MsalTokenResponse, GrantRequest, OAuthSerializer}; use test_tools::oauth::OAuthTestTool; use url::{Host, Url}; @@ -68,7 +68,7 @@ fn refresh_token_uri() { .add_scope("Fall.Down") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut access_token = AccessToken::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); + let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index cc8e1169..e9724341 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{AccessToken, GrantRequest, OAuthSerializer}; +use graph_rs_sdk::oauth::{MsalTokenResponse, GrantRequest, OAuthSerializer}; #[test] fn sign_in_code_url() { @@ -49,7 +49,7 @@ fn access_token() { .authorization_url("https://www.example.com/token") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut builder = AccessToken::default(); + let mut builder = MsalTokenResponse::default(); builder .set_token_type("token") .set_bearer_token("access_token") @@ -75,7 +75,7 @@ fn refresh_token() { .authorization_url("https://www.example.com/token") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut access_token = AccessToken::new("access_token", 3600, "Read.Write", "asfasf"); + let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); @@ -100,7 +100,7 @@ fn get_refresh_token() { .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); - let mut access_token = AccessToken::new("access_token", 3600, "Read.Write", "asfasf"); + let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); From 9326726cc14bf02b0aba613f5a3aaf2b004223a9 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 2 Aug 2023 05:53:47 -0400 Subject: [PATCH 026/118] Remove abstractions until it can be completed as its own feature --- Cargo.toml | 4 +- graph-sdk-abstractions/Cargo.toml | 26 --- graph-sdk-abstractions/src/backing_store.rs | 51 ----- graph-sdk-abstractions/src/http/mod.rs | 206 ------------------ graph-sdk-abstractions/src/lib.rs | 2 - graph-sdk-abstractions/src/policy/mod.rs | 0 .../src/policy/retry_after_policy.rs | 0 7 files changed, 1 insertion(+), 288 deletions(-) delete mode 100644 graph-sdk-abstractions/Cargo.toml delete mode 100644 graph-sdk-abstractions/src/backing_store.rs delete mode 100644 graph-sdk-abstractions/src/http/mod.rs delete mode 100644 graph-sdk-abstractions/src/lib.rs delete mode 100644 graph-sdk-abstractions/src/policy/mod.rs delete mode 100644 graph-sdk-abstractions/src/policy/retry_after_policy.rs diff --git a/Cargo.toml b/Cargo.toml index fb06e053..c6e19b0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,7 @@ members = [ "test-tools", "graph-codegen", "graph-http", - "graph-core", - "graph-sdk-abstractions" + "graph-core" ] [dependencies] @@ -71,7 +70,6 @@ from_as = "0.2.0" actix = "0.13.0" actix-rt = "2.8.0" -graph-sdk-abstractions = { path = "./graph-sdk-abstractions" } graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/graph-sdk-abstractions/Cargo.toml b/graph-sdk-abstractions/Cargo.toml deleted file mode 100644 index f38128ad..00000000 --- a/graph-sdk-abstractions/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "graph-sdk-abstractions" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -# actix = "0.13.0" -# actix-rt = "2.8.0" -http = "0.2.9" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } -tokio = { version = "1", features = ["full"] } -futures = "0.3" -tower = { version="0.4.13", features=["full"] } -tower-service = "0.3.2" -tower-layer = "0.3.2" -pin-project = "1.1.1" - -[features] -default = ["native-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] -brotli = ["reqwest/brotli"] -deflate = ["reqwest/deflate"] -trust-dns = ["reqwest/trust-dns"] diff --git a/graph-sdk-abstractions/src/backing_store.rs b/graph-sdk-abstractions/src/backing_store.rs deleted file mode 100644 index cd90a97c..00000000 --- a/graph-sdk-abstractions/src/backing_store.rs +++ /dev/null @@ -1,51 +0,0 @@ -pub struct BackingStore; - -/* -use std::marker::PhantomData; -use actix::{Actor, Context, Handler}; -use actix::dev::{MessageResponse, OneshotSender}; -use actix::prelude::Message; - -pub struct BackingStore { - response: Responses -} - -impl BackingStore { - pub fn new(t: Responses) -> BackingStore { - BackingStore { - response: t - } - } -} - -impl Actor for BackingStore { - type Context = Context<Self>; -} - -impl<M: Message<Result = Responses>> Handler<M> for BackingStore { - type Result = Responses; - - fn handle(&mut self, msg: M, ctx: &mut Self::Context) -> Self::Result { - self.response.clone() - } -} - -#[derive(Clone)] -pub enum Responses { - AccessToken(String), - RefreshToken(String) -} - -impl<A, M> MessageResponse<A, M> for Responses - where - A: Actor, - M: Message<Result = Responses>, -{ - fn handle(self, ctx: &mut A::Context, tx: Option<OneshotSender<M::Result>>) { - if let Some(tx) = tx { - tx.send(self); - } - } -} - - */ diff --git a/graph-sdk-abstractions/src/http/mod.rs b/graph-sdk-abstractions/src/http/mod.rs deleted file mode 100644 index 92995d95..00000000 --- a/graph-sdk-abstractions/src/http/mod.rs +++ /dev/null @@ -1,206 +0,0 @@ -use futures::future; -use futures::future::Ready; -use http::header::RETRY_AFTER; -use pin_project::pin_project; -use reqwest::{Request, Response}; -use std::{ - fmt, - future::Future, - pin::Pin, - task::{Context, Poll}, - time::Duration, -}; -use tokio::time::Sleep; -use tower::retry::{Policy, Retry, RetryLayer}; -use tower::{BoxError, ServiceBuilder}; -use tower_service::Service; - -#[derive(Debug, Clone)] -pub struct RetryAfterPolicy(Duration); - -impl RetryAfterPolicy { - pub fn new(timeout: Duration) -> RetryAfterPolicy { - RetryAfterPolicy(timeout) - } - - pub async fn suspend(self) -> Ready<Self> { - tokio::time::sleep(self.0).await; - return futures::future::ready(self); - } -} - -impl Default for RetryAfterPolicy { - fn default() -> Self { - RetryAfterPolicy::new(Duration::from_secs(0)) - } -} - -impl<E> Policy<reqwest::Request, reqwest::Response, E> for RetryAfterPolicy { - type Future = Ready<Self>; - - fn retry(&self, req: &Request, result: Result<&Response, &E>) -> Option<Self::Future> { - println!("INSIDE THROTTLE RETRY POLICY"); - dbg!("INSIDE THROTTLE RETRY POLICY"); - if let Ok(response) = result.as_ref() { - let header = response.headers().get(RETRY_AFTER)?; - let value_str = header.to_str().ok()?; - let sec: u64 = value_str.parse().ok()?; - return Some(tower::retry::future::ResponseFuture { - request: req, - retry: (), - state: State::Retrying, - }); - } - None - } - - fn clone_request(&self, req: &Request) -> Option<Request> { - req.try_clone() - } -} - -// https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md -pub struct HttpClient; - -impl HttpClient { - pub fn new(client: reqwest::Client) -> reqwest::Client { - let service = ServiceBuilder::new() - .retry(RetryAfterPolicy::new()) - .service(client) - .into_inner(); - return service; - } -} - -#[cfg(test)] -mod test { - use super::*; - use futures::SinkExt; - use http::StatusCode; - use reqwest::Url; - use tower::ServiceExt; - - #[tokio::test] - async fn test_throttle_retry() { - let mut service = ServiceBuilder::new() - .layer(RetryLayer::new(RetryAfterPolicy::default())) - .service(reqwest::Client::new()); - let res: Result<reqwest::Response, reqwest::Error> = service - .call(Request::new( - reqwest::Method::GET, - Url::parse("https://graph.microsoft.com/v1.0/users/id/drive").unwrap(), - )) - .await; - let response = res.unwrap(); - assert_eq!(response.status().as_u16(), 401); - } -} - -/* - -pub struct ThrottleRetry<S> { - inner: S -} - -impl<S> ThrottleRetry<S> { - fn new(inner: S) -> Self { - ThrottleRetry { inner } - } -} - -impl<S, Request> Service<Request> for ThrottleRetry<S> - where - S: Service<Request>, - S::Error: Into<BoxError> -{ - type Response = S::Response; - type Error = BoxError; - type Future = RetryFuture<S::Future>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { - self.inner.poll_ready(cx).map_err(Into::into) - } - - fn call(&mut self, req: Request) -> Self::Future { - - let response_future = self.inner.call(req); - let sleep = tokio::time::sleep(self.timeout); - - RetryFuture { - response_future, - sleep, - } - } -} - - -#[pin_project] -struct RetryFuture<F> { - #[pin] - response_future: F, - #[pin] - sleep: Sleep, -} - -impl<F, Error> Future for RetryFuture<F> - where - F: Future<Output = Result<reqwest::Response, Error>>, - Error: Into<BoxError>, -{ - type Output = Result<reqwest::Response, BoxError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { - let this = self.project(); - - match this.response_future.poll(cx) { - Poll::Ready(result) => { - let result: Result<reqwest::Response, BoxError> = result.map_err(Into::into); - - match result { - Ok(response) => { - let status = response.status(); - if status.is_success() { - return Poll::Ready(Ok(response)); - } - - // 429 Too Many Requests - // Wait on Retry-After Header - if status.as_u16().eq(&429) { - - } - - - - - return Poll::Ready(Ok(response)) - }, - Err(err) => return Poll::Ready(Err(err)) - } - } - Poll::Pending => {} - } - - Poll::Pending - } -} - - -impl<S, Request> Service<Request> for ThrottleRetry<S> -where - S: Service<Request>, - S::Error: Into<BoxError> -{ - type Response = S::Response; - type Error = BoxError; - type Future = Response; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { - self.inner.poll_ready(cx).map_err(Into::into) - } - - fn call(&mut self, req: Request) -> Self::Future { - let response_future = self.inner.call(request); - } -} - - */ diff --git a/graph-sdk-abstractions/src/lib.rs b/graph-sdk-abstractions/src/lib.rs deleted file mode 100644 index 0f5332aa..00000000 --- a/graph-sdk-abstractions/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod backing_store; -pub mod http; diff --git a/graph-sdk-abstractions/src/policy/mod.rs b/graph-sdk-abstractions/src/policy/mod.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/graph-sdk-abstractions/src/policy/retry_after_policy.rs b/graph-sdk-abstractions/src/policy/retry_after_policy.rs deleted file mode 100644 index e69de29b..00000000 From 027bd17d9de936466b04a86df76cae4a0cda6472 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 2 Aug 2023 06:06:13 -0400 Subject: [PATCH 027/118] Run Clippy and fmt --- examples/oauth/auth_code_grant.rs | 4 +- examples/oauth/auth_code_grant_pkce.rs | 4 +- examples/oauth/client_credentials.rs | 2 +- examples/oauth/device_code.rs | 20 ++-- examples/oauth/main.rs | 6 +- examples/oauth/open_id_connect.rs | 2 +- examples/oauth_certificate/main.rs | 2 +- graph-http/src/client.rs | 15 +-- graph-http/src/pipeline/http_pipeline.rs | 12 +- graph-oauth/src/access_token.rs | 13 ++- graph-oauth/src/auth.rs | 10 -- .../src/identity/application_options.rs | 104 +++++++++--------- graph-oauth/src/identity/authority.rs | 6 +- .../credentials/application_builder.rs | 67 +++++++---- .../src/identity/credentials/as_query.rs | 3 - ...thorization_code_certificate_credential.rs | 3 +- .../authorization_code_credential.rs | 4 +- .../client_certificate_credential.rs | 5 +- .../credentials/client_secret_credential.rs | 3 +- .../confidential_client_application.rs | 8 +- .../credentials/device_code_credential.rs | 18 ++- graph-oauth/src/identity/credentials/mod.rs | 2 +- .../credentials/open_id_credential.rs | 4 +- .../credentials/public_client_application.rs | 4 +- .../resource_owner_password_credential.rs | 5 +- .../identity/credentials/token_credential.rs | 4 +- .../src/identity/credentials/token_request.rs | 4 +- .../identity/credentials/x509_certificate.rs | 1 + src/client/graph.rs | 2 +- test-tools/src/oauth_request.rs | 2 +- tests/grants_authorization_code.rs | 5 +- tests/grants_code_flow.rs | 2 +- 32 files changed, 172 insertions(+), 174 deletions(-) diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 1a395620..4f98e94e 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::oauth::{ - MsalTokenResponse, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, - ConfidentialClientApplication, TokenCredential, TokenRequest, + AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, + MsalTokenResponse, TokenCredential, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index f63dc474..01d29692 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ - MsalTokenResponse, AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, - ConfidentialClientApplication, ProofKeyForCodeExchange, TokenCredential, TokenRequest, + AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, + MsalTokenResponse, ProofKeyForCodeExchange, TokenCredential, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index d706f94b..1d561860 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -10,7 +10,7 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - MsalTokenResponse, ClientSecretCredential, ConfidentialClientApplication, TokenCredential, + ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, TokenCredential, TokenRequest, }; diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 5cae5262..1d89c46a 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -101,6 +101,7 @@ async fn poll_for_access_token( // The authorization url for device code must be https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode // where tenant can be common, pub async fn device_code() -> GraphResult<()> { + /* let mut credential = device_code_credential(); let response = credential.get_token_async().await?; @@ -111,6 +112,7 @@ pub async fn device_code() -> GraphResult<()> { let device_code = json["device_code"].as_str().unwrap(); let interval = json["interval"].as_u64().unwrap(); let message = json["message"].as_str().unwrap(); + */ /* The authorization request is a POST and a successful response body will look similar to: @@ -125,6 +127,7 @@ pub async fn device_code() -> GraphResult<()> { } */ + /* // Print the message to the user who needs to sign in: println!("{message:#?}"); @@ -133,16 +136,19 @@ pub async fn device_code() -> GraphResult<()> { let access_token_json = poll_for_access_token(device_code, interval, message).await?; let access_token: MsalTokenResponse = serde_json::from_value(access_token_json)?; println!("{access_token:#?}"); + */ - // Get a refresh token. First pass the access token to the oauth instance. - oauth.access_token(access_token); - let mut handler = oauth.build_async().device_code(); + /* + // Get a refresh token. First pass the access token to the oauth instance. + oauth.access_token(access_token); + let mut handler = oauth.build_async().device_code(); - let response = handler.refresh_token().send().await?; - println!("{response:#?}"); + let response = handler.refresh_token().send().await?; + println!("{response:#?}"); - let body: serde_json::Value = response.json().await?; - println!("{body:#?}"); + let body: serde_json::Value = response.json().await?; + println!("{body:#?}"); + */ Ok(()) } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index f923712c..e9063914 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -29,10 +29,10 @@ mod open_id_connect; mod signing_keys; use graph_rs_sdk::oauth::{ - MsalTokenResponse, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, ProofKeyForCodeExchange, PublicClientApplication, TokenCredential, - TokenRequest, + DeviceCodeCredential, MsalTokenResponse, ProofKeyForCodeExchange, PublicClientApplication, + TokenCredential, TokenRequest, }; #[tokio::main] diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index d4937195..cdd4944c 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,6 +1,6 @@ use graph_oauth::identity::{ResponseType, TokenCredential, TokenRequest}; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; -use graph_rs_sdk::oauth::{MsalTokenResponse, IdToken, OAuthSerializer}; +use graph_rs_sdk::oauth::{IdToken, MsalTokenResponse, OAuthSerializer}; use url::Url; /// # Example /// ``` diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 8234f649..18323440 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,7 +4,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ - MsalTokenResponse, AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, MsalTokenResponse, PKey, TokenCredential, X509Certificate, X509, }; use std::fs::File; diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 826e56d6..893014ba 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -9,7 +9,7 @@ use std::time::Duration; fn user_agent_header_from_env() -> Option<HeaderValue> { let header = std::option_env!("GRAPH_CLIENT_USER_AGENT")?; - HeaderValue::from_str(&header).ok() + HeaderValue::from_str(header).ok() } #[derive(Clone)] @@ -138,15 +138,6 @@ impl GraphClientConfiguration { self } - fn retain_agent_header(&self) -> Option<HeaderValue> { - if !self.config.headers.contains_key(USER_AGENT) { - let header = std::env::var(USER_AGENT.as_str()).ok()?; - HeaderValue::from_str(&header).ok() - } else { - None - } - } - pub fn build(self) -> Client { let config = self.clone(); let headers = self.config.headers.clone(); @@ -260,7 +251,7 @@ mod test { #[test] fn compile_time_user_agent_header() { - let mut client = GraphClientConfiguration::new() + let client = GraphClientConfiguration::new() .access_token("access_token") .build(); @@ -269,7 +260,7 @@ mod test { #[test] fn update_user_agent_header() { - let mut client = GraphClientConfiguration::new() + let client = GraphClientConfiguration::new() .access_token("access_token") .user_agent(HeaderValue::from_static("user_agent")) .build(); diff --git a/graph-http/src/pipeline/http_pipeline.rs b/graph-http/src/pipeline/http_pipeline.rs index 547d96b2..d3956c5e 100644 --- a/graph-http/src/pipeline/http_pipeline.rs +++ b/graph-http/src/pipeline/http_pipeline.rs @@ -39,9 +39,9 @@ pub struct ExponentialBackoffRetryPolicy { impl HttpPipelinePolicy for ExponentialBackoffRetryPolicy { fn process_async( &self, - context: &RequestContext, - request: &mut Request<Value>, - pipeline: &[Arc<dyn HttpPipelinePolicy>], + _context: &RequestContext, + _request: &mut Request<Value>, + _pipeline: &[Arc<dyn HttpPipelinePolicy>], ) -> Result<SomePolicyResult, Box<dyn Error>> { // modify request... @@ -56,9 +56,9 @@ pub struct ThrottleRetryPolicy { impl HttpPipelinePolicy for ThrottleRetryPolicy { fn process_async( &self, - context: &RequestContext, - request: &mut Request<Value>, - pipeline: &[Arc<dyn HttpPipelinePolicy>], + _context: &RequestContext, + _request: &mut Request<Value>, + _pipeline: &[Arc<dyn HttpPipelinePolicy>], ) -> Result<SomePolicyResult, Box<dyn Error>> { // modify request... diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 617d53d7..381b4658 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -82,11 +82,16 @@ pub struct MsalTokenResponse { } impl MsalTokenResponse { - pub fn new(token_type: &str, expires_in: i64, scope: &str, access_token: &str) -> MsalTokenResponse { + pub fn new( + token_type: &str, + expires_in: i64, + scope: &str, + access_token: &str, + ) -> MsalTokenResponse { MsalTokenResponse { token_type: token_type.into(), - ext_expires_in: Some(expires_in.clone()), - expires_in: expires_in.clone(), + ext_expires_in: Some(expires_in), + expires_in, scope: Some(scope.into()), access_token: access_token.into(), refresh_token: None, @@ -421,7 +426,7 @@ impl<'de> Deserialize<'de> for MsalTokenResponse { Ok(MsalTokenResponse { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, - expires_in: phantom_access_token.expires_in.clone(), + expires_in: phantom_access_token.expires_in, ext_expires_in: phantom_access_token.ext_expires_in, scope: phantom_access_token.scope, refresh_token: phantom_access_token.refresh_token, diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 8d312ecc..20e0a7b5 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -121,16 +121,6 @@ impl AsRef<str> for OAuthParameter { } } -pub struct OAuth2Client { - headers: HashMap<String, String>, - query_parameters: HashMap<String, String>, - body_parameters: HashMap<String, String>, -} - -impl OAuth2Client { - pub fn new(logger: impl log::Log) {} -} - /// Serializer for query/x-www-form-urlencoded OAuth requests. /// /// OAuth Serializer for query/form serialization that supports the OAuth 2.0 and OpenID diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 229c1d67..5e8e1d39 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -1,63 +1,63 @@ /* - /// <summary> - /// Client ID (also known as App ID) of the application as registered in the - /// application registration portal (https://aka.ms/msal-net-register-app) - /// </summary> - public string ClientId { get; set; } + /// <summary> + /// Client ID (also known as App ID) of the application as registered in the + /// application registration portal (https://aka.ms/msal-net-register-app) + /// </summary> + public string ClientId { get; set; } - /// <summary> - /// Tenant from which the application will allow users to sign it. This can be: - /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). - /// This property is mutually exclusive with <see cref="AadAuthorityAudience"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - /// <remarks>The name of the property was chosen to ensure compatibility with AzureAdOptions - /// in ASP.NET Core configuration files (even the semantics would be tenant)</remarks> - public string TenantId { get; set; } + /// <summary> + /// Tenant from which the application will allow users to sign it. This can be: + /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). + /// This property is mutually exclusive with <see cref="AadAuthorityAudience"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + /// <remarks>The name of the property was chosen to ensure compatibility with AzureAdOptions + /// in ASP.NET Core configuration files (even the semantics would be tenant)</remarks> + public string TenantId { get; set; } - /// <summary> - /// Sign-in audience. This property is mutually exclusive with TenantId. If both - /// are provided, an exception will be thrown. - /// </summary> - public AadAuthorityAudience AadAuthorityAudience { get; set; } = AadAuthorityAudience.None; + /// <summary> + /// Sign-in audience. This property is mutually exclusive with TenantId. If both + /// are provided, an exception will be thrown. + /// </summary> + public AadAuthorityAudience AadAuthorityAudience { get; set; } = AadAuthorityAudience.None; - /// <summary> - /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). - /// The name was chosen to ensure compatibility with AzureAdOptions in ASP.NET Core. - /// This property is mutually exclusive with <see cref="AzureCloudInstance"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - public string Instance { get; set; } + /// <summary> + /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). + /// The name was chosen to ensure compatibility with AzureAdOptions in ASP.NET Core. + /// This property is mutually exclusive with <see cref="AzureCloudInstance"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + public string Instance { get; set; } - /// <summary> - /// Specific instance in the case of Azure Active Directory. - /// It allows users to use the enum instead of the explicit URL. - /// This property is mutually exclusive with <see cref="Instance"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - public AzureCloudInstance AzureCloudInstance { get; set; } = AzureCloudInstance.None; + /// <summary> + /// Specific instance in the case of Azure Active Directory. + /// It allows users to use the enum instead of the explicit URL. + /// This property is mutually exclusive with <see cref="Instance"/>. If both + /// are provided, an exception will be thrown. + /// </summary> + public AzureCloudInstance AzureCloudInstance { get; set; } = AzureCloudInstance.None; - /// <summary> - /// This redirect URI needs to be registered in the app registration. See https://aka.ms/msal-net-register-app for - /// details on which redirect URIs are defined by default by MSAL.NET and how to register them. - /// Also use: <see cref="PublicClientApplicationBuilder.WithDefaultRedirectUri"/> which provides - /// a good default for public client applications for all platforms. - /// - /// For web apps and web APIs, the redirect URI is computed from the URL where the application is running - /// (for instance, <c>baseUrl//signin-oidc</c> for ASP.NET Core web apps). - /// - /// For daemon applications (confidential client applications using only the Client Credential flow - /// that is calling <c>AcquireTokenForClient</c>), no reply URI is needed. - /// </summary> - /// <remarks>This is especially important when you deploy an application that you have initially tested locally; - /// you then need to add the reply URL of the deployed application in the application registration portal - /// </remarks> - public string RedirectUri { get; set; } - */ + /// <summary> + /// This redirect URI needs to be registered in the app registration. See https://aka.ms/msal-net-register-app for + /// details on which redirect URIs are defined by default by MSAL.NET and how to register them. + /// Also use: <see cref="PublicClientApplicationBuilder.WithDefaultRedirectUri"/> which provides + /// a good default for public client applications for all platforms. + /// + /// For web apps and web APIs, the redirect URI is computed from the URL where the application is running + /// (for instance, <c>baseUrl//signin-oidc</c> for ASP.NET Core web apps). + /// + /// For daemon applications (confidential client applications using only the Client Credential flow + /// that is calling <c>AcquireTokenForClient</c>), no reply URI is needed. + /// </summary> + /// <remarks>This is especially important when you deploy an application that you have initially tested locally; + /// you then need to add the reply URL of the deployed application in the application registration portal + /// </remarks> + public string RedirectUri { get; set; } +*/ -use url::Url; use crate::identity::AadAuthorityAudience; use crate::oauth::AzureCloudInstance; +use url::Url; /// Application Options typically stored as JSON file in .net applications. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] @@ -77,5 +77,5 @@ pub struct ApplicationOptions { #[serde(alias = "instance", alias = "Instance")] pub instance: Option<Url>, pub azure_cloud_instance: Option<AzureCloudInstance>, - pub redirect_uri: Option<Url> + pub redirect_uri: Option<Url>, } diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 71ad5538..327e1b91 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -2,7 +2,9 @@ use url::Url; /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). /// Maps to the instance url string. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, +)] pub enum AzureCloudInstance { // Custom Value communicating that the AzureCloudInstance. //Custom(String), @@ -91,7 +93,7 @@ pub enum AadAuthorityAudience { /// Users with a personal Microsoft account. Maps to https://[AzureCloudInstance]/consumers/ /// or https://[instance]/consumers/ - PersonalMicrosoftAccount + PersonalMicrosoftAccount, } /// Specifies which Microsoft accounts can be used for sign-in with a given application. diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index fd03bdff..97a671e4 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,19 +1,16 @@ -use std::collections::HashMap; -use anyhow::{anyhow, ensure}; -use reqwest::header::HeaderMap; -use url::Url; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; -use crate::identity::{Authority}; -use crate::identity::application_options::ApplicationOptions; -use crate::oauth::{AzureCloudInstance, ConfidentialClientApplication}; +use crate::identity::{application_options::ApplicationOptions, Authority, AzureCloudInstance}; +use reqwest::header::HeaderMap; +use std::collections::HashMap; +use url::Url; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). /// Maps to the instance url string. AzureCloudInstance(AzureCloudInstance), - Uri(Url) + Uri(Url), } impl From<AzureCloudInstance> for AuthorityHost { @@ -36,9 +33,9 @@ impl Default for AuthorityHost { pub enum ClientCredentialParameter { #[cfg(feature = "openssl")] - CertificateClientCredential(X509Certificate), - SecretStringClientCredential(String), - SignedAssertionClientCredential(String), + Certificate(X509Certificate), + SecretString(String), + SignedAssertion(String), } pub struct ConfidentialClientApplicationBuilder { @@ -68,7 +65,9 @@ impl ConfidentialClientApplicationBuilder { } } - pub fn create_with_application_options(application_options: ApplicationOptions) -> anyhow::Result<ConfidentialClientApplicationBuilder> { + pub fn create_with_application_options( + application_options: ApplicationOptions, + ) -> anyhow::Result<ConfidentialClientApplicationBuilder> { ConfidentialClientApplicationBuilder::try_from(application_options) } @@ -100,7 +99,10 @@ impl ConfidentialClientApplicationBuilder { self } - pub fn with_azure_cloud_instance(&mut self, azure_cloud_instance: AzureCloudInstance) -> &mut Self { + pub fn with_azure_cloud_instance( + &mut self, + azure_cloud_instance: AzureCloudInstance, + ) -> &mut Self { self.authority_url = AuthorityHost::AzureCloudInstance(azure_cloud_instance); self } @@ -117,22 +119,30 @@ impl ConfidentialClientApplicationBuilder { } #[cfg(feature = "openssl")] - pub fn with_certificate(&mut self, certificate: X509Certificate) -> &mut self { - self.client_credential_parameter = Some(ClientCredentialParameter::CertificateClientCredential(certificate)); + pub fn with_certificate(&mut self, certificate: X509Certificate) -> &mut Self { + self.client_credential_parameter = + Some(ClientCredentialParameter::Certificate(certificate)); self } pub fn with_client_secret(&mut self, client_secret: impl AsRef<str>) -> &mut Self { - self.client_credential_parameter = Some(ClientCredentialParameter::SecretStringClientCredential(client_secret.as_ref().to_owned())); + self.client_credential_parameter = Some(ClientCredentialParameter::SecretString( + client_secret.as_ref().to_owned(), + )); self } pub fn with_signed_assertion(&mut self, signed_assertion: impl AsRef<str>) -> &mut Self { - self.client_credential_parameter = Some(ClientCredentialParameter::SignedAssertionClientCredential(signed_assertion.as_ref().to_owned())); + self.client_credential_parameter = Some(ClientCredentialParameter::SignedAssertion( + signed_assertion.as_ref().to_owned(), + )); self } - pub fn with_extra_query_parameters(&mut self, query_parameters: HashMap<String, String>) -> &mut Self { + pub fn with_extra_query_parameters( + &mut self, + query_parameters: HashMap<String, String>, + ) -> &mut Self { self.extra_query_parameters = query_parameters; self } @@ -148,16 +158,26 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { anyhow::ensure!(value.client_id.is_empty(), "Client id cannot be empty"); - anyhow::ensure!(!(value.instance.is_some() && value.azure_cloud_instance.is_some()), "Instance and AzureCloudInstance cannot both be set"); - anyhow::ensure!(!(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), "TenantId and AadAuthorityAudience cannot both be set"); + anyhow::ensure!( + !(value.instance.is_some() && value.azure_cloud_instance.is_some()), + "Instance and AzureCloudInstance cannot both be set" + ); + anyhow::ensure!( + !(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), + "TenantId and AadAuthorityAudience cannot both be set" + ); let default_redirect_uri = value.redirect_uri.is_none(); Ok(ConfidentialClientApplicationBuilder { client_id: value.client_id, tenant_id: value.tenant_id, - authority: value.aad_authority_audience.map(|aud| Authority::from(aud)) + authority: value + .aad_authority_audience + .map(Authority::from) .unwrap_or_default(), - authority_url: value.azure_cloud_instance.map(|aci| AuthorityHost::AzureCloudInstance(aci)) + authority_url: value + .azure_cloud_instance + .map(AuthorityHost::AzureCloudInstance) .unwrap_or_default(), redirect_uri: value.redirect_uri, default_redirect_uri, @@ -182,6 +202,7 @@ mod test { instance: Some(Url::parse("https://login.microsoft.com").unwrap()), azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), redirect_uri: None, - }).unwrap(); + }) + .unwrap(); } } diff --git a/graph-oauth/src/identity/credentials/as_query.rs b/graph-oauth/src/identity/credentials/as_query.rs index 5db21333..dd32c910 100644 --- a/graph-oauth/src/identity/credentials/as_query.rs +++ b/graph-oauth/src/identity/credentials/as_query.rs @@ -5,7 +5,6 @@ pub trait AsQuery<RHS = Self> { impl<T: ToString + Clone> AsQuery for std::slice::Iter<'_, T> { fn as_query(&self) -> String { self.clone() - .into_iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(" ") @@ -15,7 +14,6 @@ impl<T: ToString + Clone> AsQuery for std::slice::Iter<'_, T> { impl<T: ToString + Clone> AsQuery for std::collections::hash_set::Iter<'_, T> { fn as_query(&self) -> String { self.clone() - .into_iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(" ") @@ -25,7 +23,6 @@ impl<T: ToString + Clone> AsQuery for std::collections::hash_set::Iter<'_, T> { impl<T: ToString + Clone> AsQuery for std::collections::btree_set::Iter<'_, T> { fn as_query(&self) -> String { self.clone() - .into_iter() .map(|s| s.to_string()) .collect::<Vec<String>>() .join(" ") diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 6d14a5c1..28696dd0 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,8 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AuthorizationSerializer, AzureCloudInstance, TokenCredential, TokenCredentialOptions, - TokenRequest, CLIENT_ASSERTION_TYPE, + AzureCloudInstance, TokenCredential, TokenCredentialOptions, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 6c18d873..b89478b6 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - AuthCodeAuthorizationUrlParameters, Authority, AuthorizationSerializer, AzureCloudInstance, - ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, + AuthCodeAuthorizationUrlParameters, Authority, AzureCloudInstance, ProofKeyForCodeExchange, + TokenCredential, TokenCredentialOptions, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 3c03abb3..14a63e54 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,8 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, - TokenCredentialOptions, TokenRequest, -}; +use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 663dc666..d519f1df 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,7 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, - ClientCredentialsAuthorizationUrlBuilder, TokenCredential, TokenRequest, + Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, TokenCredential, }; use crate::oauth::TokenCredentialOptions; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 7c35b0c0..0cfcf57f 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,8 +1,8 @@ use crate::identity::{ - AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AuthorizationSerializer, - AzureCloudInstance, ClientApplication, ClientCertificateCredential, ClientSecretCredential, - CredentialStore, CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, - TokenCacheProviderType, TokenCredential, TokenCredentialOptions, TokenRequest, + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AzureCloudInstance, + ClientApplication, ClientCertificateCredential, ClientSecretCredential, CredentialStore, + CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, TokenCacheProviderType, + TokenCredential, TokenCredentialOptions, }; use crate::oauth::UnInitializedCredentialStore; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 84d8f819..e73cbf9a 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,15 +1,11 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, - TokenCredentialOptions, TokenRequest, -}; -use crate::oauth::{DeviceCode, PublicClientApplication}; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult, AF}; -use reqwest::Response; +use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; +use crate::oauth::DeviceCode; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; + use std::collections::HashMap; -use std::time::Duration; + use url::Url; -use wry::http; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -233,14 +229,14 @@ impl TokenCredential for DeviceCodeCredential { self.serializer.grant_type(DEVICE_CODE_GRANT_TYPE); - return self.serializer.as_credential_map( + self.serializer.as_credential_map( vec![], vec![ OAuthParameter::ClientId, OAuthParameter::Scope, OAuthParameter::GrantType, ], - ); + ) } fn client_id(&self) -> &String { diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 9c15685e..d4565fa8 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -3,6 +3,7 @@ mod credential_builder; pub mod legacy; +mod application_builder; mod as_query; mod auth_code_authorization_url_parameters; mod authorization_code_certificate_credential; @@ -12,7 +13,6 @@ mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; -mod application_builder; mod crypto; mod device_code_credential; mod display; diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index b8acda9e..afd7a50d 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, OpenIdAuthorizationUrl, - ProofKeyForCodeExchange, TokenCredential, TokenCredentialOptions, TokenRequest, + Authority, AzureCloudInstance, OpenIdAuthorizationUrl, ProofKeyForCodeExchange, + TokenCredential, TokenCredentialOptions, }; use crate::oauth::OpenIdAuthorizationUrlBuilder; use async_trait::async_trait; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 289987e0..59e58880 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,6 +1,6 @@ use crate::identity::{ - AuthorizationSerializer, AzureCloudInstance, DeviceCodeCredential, - ResourceOwnerPasswordCredential, TokenCredential, TokenCredentialOptions, TokenRequest, + AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredential, + TokenCredentialOptions, }; use async_trait::async_trait; use graph_error::AuthorizationResult; diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index a9c5d186..a29ad4a4 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,8 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, TokenCredential, - TokenCredentialOptions, TokenRequest, -}; +use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential.rs index 702d27ae..8126b2fc 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential.rs @@ -1,6 +1,4 @@ -use crate::identity::{ - AuthorizationSerializer, AzureCloudInstance, TokenCredentialOptions, TokenRequest, -}; +use crate::identity::{AzureCloudInstance, TokenCredentialOptions}; use async_trait::async_trait; use graph_error::AuthorizationResult; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 21a4b733..aa3e222c 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,11 +1,9 @@ -use crate::identity::{AzureCloudInstance, TokenCredential}; use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; -use graph_error::AuthorizationResult; + use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; -use url::Url; #[async_trait] pub trait TokenRequest: AuthorizationSerializer { diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index bd4fa90d..7ca4b882 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -26,6 +26,7 @@ fn encode_cert_ref(cert: &X509Ref) -> anyhow::Result<String> { )) } +#[allow(unused)] fn thumbprint(cert: &X509) -> anyhow::Result<String> { let digest_bytes = cert .digest(MessageDigest::sha1()) diff --git a/src/client/graph.rs b/src/client/graph.rs index 2b1057cd..9647fc83 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{MsalTokenResponse, AllowedHostValidator, HostValidator, OAuthSerializer}; +use crate::oauth::{AllowedHostValidator, HostValidator, MsalTokenResponse, OAuthSerializer}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 61303dec..a952622a 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,7 +3,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - MsalTokenResponse, ClientSecretCredential, ResourceOwnerPasswordCredential, TokenCredential, + ClientSecretCredential, MsalTokenResponse, ResourceOwnerPasswordCredential, TokenCredential, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index e663f859..445972db 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{MsalTokenResponse, GrantRequest, OAuthSerializer}; +use graph_rs_sdk::oauth::{GrantRequest, MsalTokenResponse, OAuthSerializer}; use test_tools::oauth::OAuthTestTool; use url::{Host, Url}; @@ -68,7 +68,8 @@ fn refresh_token_uri() { .add_scope("Fall.Down") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); + let mut access_token = + MsalTokenResponse::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index e9724341..beeac811 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{MsalTokenResponse, GrantRequest, OAuthSerializer}; +use graph_rs_sdk::oauth::{GrantRequest, MsalTokenResponse, OAuthSerializer}; #[test] fn sign_in_code_url() { From 146a9c8e89dad6c9ac511361d5305cebebdb8ba8 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 2 Aug 2023 08:43:48 -0400 Subject: [PATCH 028/118] Clena up test tools AppRegistrations --- examples/oauth/open_id_connect.rs | 1 + graph-oauth/src/auth.rs | 58 +------------------ .../credentials/application_builder.rs | 15 +++++ test-tools/Cargo.toml | 1 + test-tools/src/oauth_request.rs | 24 +++++--- tests/upload_request_blocking.rs | 2 +- 6 files changed, 38 insertions(+), 63 deletions(-) diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index cdd4944c..f27c1266 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -44,6 +44,7 @@ fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Re .build() .url()?) } + #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct OpenIdResponse { pub code: String, diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 20e0a7b5..c4dbbfd0 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -969,61 +969,6 @@ impl OAuthSerializer { t: PhantomData, } } - - /// Sign the user out using the OneDrive v1.0 endpoint. - /// - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::OAuth; - /// let mut oauth = OAuth::new(); - /// - /// oauth.v1_logout().unwrap(); - /// ``` - pub fn v1_logout(&mut self) -> GraphResult<()> { - let mut url = self.get_or_else(OAuthParameter::LogoutURL)?; - if !url.ends_with('?') { - url.push('?'); - } - - let mut vec = vec![ - url, - "&client_id=".to_string(), - self.get_or_else(OAuthParameter::ClientId)?, - "&redirect_uri=".to_string(), - ]; - - if let Some(redirect) = self.get(OAuthParameter::PostLogoutRedirectURI) { - vec.push(redirect); - } else if let Some(redirect) = self.get(OAuthParameter::RedirectUri) { - vec.push(redirect); - } - webbrowser::open(vec.join("").as_str()).map_err(GraphFailure::from) - } - - /// Sign the user out using the OneDrive v2.0 endpoint. - /// - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::OAuth; - /// let mut oauth = OAuth::new(); - /// - /// oauth.v2_logout().unwrap(); - /// ``` - pub fn v2_logout(&self) -> GraphResult<()> { - let mut url = self.get_or_else(OAuthParameter::LogoutURL)?; - if !url.ends_with('?') { - url.push('?'); - } - if let Some(redirect) = self.get(OAuthParameter::PostLogoutRedirectURI) { - url.push_str("post_logout_redirect_uri="); - url.push_str(redirect.as_str()); - } else { - let redirect_uri = self.get_or_else(OAuthParameter::RedirectUri)?; - url.push_str("post_logout_redirect_uri="); - url.push_str(redirect_uri.as_str()); - } - webbrowser::open(url.as_str()).map_err(GraphFailure::from) - } } impl OAuthSerializer { @@ -1037,6 +982,9 @@ impl OAuthSerializer { pub fn try_as_tuple(&self, oac: &OAuthParameter) -> AuthorizationResult<(String, String)> { if oac.eq(&OAuthParameter::Scope) { + if self.scopes.is_empty() { + return Err(AuthorizationFailure::required(oac)); + } Ok((oac.alias().to_owned(), self.join_scopes(" "))) } else { Ok(( diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 97a671e4..a9f2b46a 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -191,6 +191,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { #[cfg(test)] mod test { use super::*; + use crate::oauth::AadAuthorityAudience; #[test] #[should_panic] @@ -205,4 +206,18 @@ mod test { }) .unwrap(); } + + #[test] + #[should_panic] + fn error_result_on_tenant_id_and_aad_audience() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } } diff --git a/test-tools/Cargo.toml b/test-tools/Cargo.toml index e90038de..f2b6025d 100644 --- a/test-tools/Cargo.toml +++ b/test-tools/Cargo.toml @@ -9,6 +9,7 @@ description = "Microsoft Graph Api Client" publish = false [dependencies] +anyhow = { version = "1.0.69", features = ["backtrace"]} futures = "0.3" from_as = "0.2.0" lazy_static = "1.4.0" diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index a952622a..977f51a4 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -10,10 +10,11 @@ use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; +use std::path::Path; use std::sync::Mutex; // static mutex's that are used for preventing test failures -// due to too many concurrent requests for Microsoft Graph. +// due to too many concurrent requests (throttling) for Microsoft Graph. lazy_static! { pub static ref THROTTLE_MUTEX: Mutex<()> = Mutex::new(()); pub static ref DRIVE_THROTTLE_MUTEX: Mutex<()> = Mutex::new(()); @@ -25,7 +26,6 @@ lazy_static! { pub enum TestEnv { AppVeyor, GitHub, - TravisCI, #[default] Local, } @@ -35,7 +35,6 @@ impl TestEnv { match self { TestEnv::AppVeyor => Environment::is_appveyor(), TestEnv::GitHub => Environment::is_github(), - TestEnv::TravisCI => Environment::is_travis(), TestEnv::Local => Environment::is_local(), } } @@ -368,7 +367,7 @@ pub struct AppRegistrationClient { permissions: Vec<String>, test_envs: Vec<TestEnv>, test_resources: Vec<ResourceIdentity>, - clients: OAuthTestClientMap, + clients: HashMap<OAuthTestClient, OAuthTestCredentials>, } impl AppRegistrationClient { @@ -383,7 +382,7 @@ impl AppRegistrationClient { permissions, test_envs, test_resources, - clients: OAuthTestClientMap::new(), + clients: HashMap::new(), } } @@ -392,11 +391,22 @@ impl AppRegistrationClient { } pub fn get(&self, client: &OAuthTestClient) -> Option<OAuthTestCredentials> { - self.clients.get(client) + self.clients.get(client).cloned() } pub fn default_client(&self) -> Option<(OAuthTestClient, OAuthTestCredentials)> { - self.clients.get_any() + let client = self.get(&OAuthTestClient::ClientCredentials); + if client.is_none() { + self.get(&OAuthTestClient::ResourceOwnerPasswordCredentials) + .map(|credentials| { + ( + OAuthTestClient::ResourceOwnerPasswordCredentials, + credentials, + ) + }) + } else { + client.map(|credentials| (OAuthTestClient::ClientCredentials, credentials)) + } } } diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 49eaaae1..5dfcad47 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::*; -use std::thread; use std::time::Duration; +use std::{env, thread}; use test_tools::oauth_request::OAuthTestClient; fn get_special_folder_id(user_id: &str, folder: &str, client: &Graph) -> GraphResult<String> { From 84fb4c2f32cabe6c98a7ae9c1974c493c479c670 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:40:59 -0400 Subject: [PATCH 029/118] Implement Application Builders for Confidential Client and Public Client Applications --- examples/oauth/README.md | 26 ++ examples/oauth/auth_code_grant.rs | 34 +- examples/oauth/auth_code_grant_pkce.rs | 29 +- .../oauth/auth_code_grant_refresh_token.rs | 9 +- examples/oauth/client_credentials.rs | 17 +- examples/oauth/device_code.rs | 136 +------ examples/oauth/environment_credential.rs | 4 +- examples/oauth/implicit_grant.rs | 15 +- examples/oauth/main.rs | 11 +- examples/oauth/open_id_connect.rs | 9 +- examples/oauth_certificate/main.rs | 9 +- graph-oauth/Cargo.toml | 1 + graph-oauth/src/auth.rs | 50 +-- graph-oauth/src/discovery/graph_discovery.rs | 8 +- .../src/identity/application_options.rs | 90 ++--- graph-oauth/src/identity/authority.rs | 6 - .../src/identity/credentials/app_config.rs | 61 +++ .../credentials/application_builder.rs | 381 ++++++++++++++---- ...thorization_code_certificate_credential.rs | 157 ++++++-- .../authorization_code_credential.rs | 308 ++++++++------ .../credentials/client_application.rs | 8 +- .../client_assertion_credential.rs | 177 ++++++++ .../credentials/client_builder_impl.rs | 98 +++++ .../client_certificate_credential.rs | 122 ++++-- .../credentials/client_secret_credential.rs | 100 +++-- .../confidential_client_application.rs | 189 ++++----- .../credentials/credential_builder.rs | 49 --- .../credentials/device_code_credential.rs | 56 +-- .../credentials/environment_credential.rs | 70 ++-- .../credentials/implicit_credential.rs | 98 ++--- .../legacy/code_flow_credential.rs | 9 +- graph-oauth/src/identity/credentials/mod.rs | 10 +- .../credentials/open_id_authorization_url.rs | 8 +- .../credentials/open_id_credential.rs | 86 ++-- .../credentials/public_client_application.rs | 77 ++-- .../resource_owner_password_credential.rs | 69 ++-- ...ential.rs => token_credential_executor.rs} | 56 ++- .../src/identity/credentials/token_request.rs | 13 +- graph-oauth/src/lib.rs | 1 - src/lib.rs | 2 +- test-tools/src/oauth.rs | 4 +- test-tools/src/oauth_request.rs | 18 +- .../application_options/aad_options.json | 4 + tests/discovery_tests.rs | 6 +- tests/grants_code_flow.rs | 4 +- tests/oauth_tests.rs | 8 +- 46 files changed, 1651 insertions(+), 1052 deletions(-) create mode 100644 examples/oauth/README.md create mode 100644 graph-oauth/src/identity/credentials/app_config.rs create mode 100644 graph-oauth/src/identity/credentials/client_assertion_credential.rs create mode 100644 graph-oauth/src/identity/credentials/client_builder_impl.rs delete mode 100644 graph-oauth/src/identity/credentials/credential_builder.rs rename graph-oauth/src/identity/credentials/{token_credential.rs => token_credential_executor.rs} (58%) create mode 100644 test_files/application_options/aad_options.json diff --git a/examples/oauth/README.md b/examples/oauth/README.md new file mode 100644 index 00000000..f62e5653 --- /dev/null +++ b/examples/oauth/README.md @@ -0,0 +1,26 @@ +# OAuth Overview + +### Authorization Code Grant + +Getting the Confidential Client + +```rust +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ConfidentialClientApplication, +}; + +fn main() { + let authorization_code = "<AUTH_CODE>"; + let client_id = "<CLIENT_ID>"; + let client_secret = "<CLIENT_SECRET>"; + + let auth_code_credential = AuthorizationCodeCredential::builder(authorization_code) + .with_client_id(client_id) + .with_client_secret(client_secret) + .with_scope(vec!["files.read", "offline_access"]) + .with_redirect_uri("http://localhost:8000/redirect")? + .build(); + + let confidential_client = ConfidentialClientApplication::from(auth_code_credential); +} +``` diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant.rs index 4f98e94e..dfc856df 100644 --- a/examples/oauth/auth_code_grant.rs +++ b/examples/oauth/auth_code_grant.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalTokenResponse, TokenCredential, TokenRequest, + MsalTokenResponse, TokenCredentialExecutor, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; @@ -25,20 +25,6 @@ pub fn authorization_sign_in() { webbrowser::open(url.as_str()).unwrap(); } -pub fn get_confidential_client( - authorization_code: &str, -) -> anyhow::Result<ConfidentialClientApplication> { - let auth_code_credential = AuthorizationCodeCredential::builder() - .with_authorization_code(authorization_code) - .with_client_id(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_scope(vec!["files.read", "offline_access"]) - .with_redirect_uri("http://localhost:8000/redirect")? - .build(); - - Ok(ConfidentialClientApplication::from(auth_code_credential)) -} - /// # Example /// ``` /// use graph_rs_sdk::*: @@ -71,16 +57,20 @@ async fn handle_redirect( // Print out the code for debugging purposes. println!("{access_code:#?}"); + let authorization_code = access_code.code; + // Set the access code and request an access token. // Callers should handle the Result from requesting an access token // in case of an error here. - let mut confidential_client_application = - get_confidential_client(access_code.code.as_str()).unwrap(); - - let response = confidential_client_application - .get_token_async() - .await - .unwrap(); + let mut confidential_client = AuthorizationCodeCredential::builder(authorization_code) + .with_client_id(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_scope(vec!["files.read", "offline_access"]) + .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() + .build(); + + let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); if response.status().is_success() { diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant_pkce.rs index 01d29692..8035ecef 100644 --- a/examples/oauth/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant_pkce.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalTokenResponse, ProofKeyForCodeExchange, TokenCredential, TokenRequest, + MsalTokenResponse, ProofKeyForCodeExchange, TokenCredentialExecutor, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; @@ -43,21 +43,6 @@ fn authorization_sign_in() { webbrowser::open(url.as_str()).unwrap(); } -/// Build the Authorization Code Grant Credential. -fn get_confidential_client_application( - authorization_code: &str, -) -> anyhow::Result<ConfidentialClientApplication> { - let credential = AuthorizationCodeCredential::builder() - .with_authorization_code(authorization_code) - .with_client_id(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_redirect_uri("http://localhost:8000/redirect")? - .with_proof_key_for_code_exchange(&PKCE) - .build(); - - Ok(ConfidentialClientApplication::from(credential)) -} - // When the authorization code comes in on the redirect from sign in, call the get_credential // method passing in the authorization code. The AuthorizationCodeCredential can be passed // to a confidential client application in order to exchange the authorization code @@ -70,11 +55,17 @@ async fn handle_redirect( // Print out the code for debugging purposes. println!("{:#?}", access_code.code); - let mut confidential_client = - get_confidential_client_application(access_code.code.as_str()).unwrap(); + let authorization_code = access_code.code; + let mut confidential_client = AuthorizationCodeCredential::builder(authorization_code) + .with_client_id(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() + .with_pkce(&PKCE) + .build(); // Returns reqwest::Response - let response = confidential_client.get_token_async().await.unwrap(); + let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); if response.status().is_success() { diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant_refresh_token.rs index 3d0f0896..cb6c2dd1 100644 --- a/examples/oauth/auth_code_grant_refresh_token.rs +++ b/examples/oauth/auth_code_grant_refresh_token.rs @@ -1,6 +1,7 @@ use graph_oauth::identity::AuthorizationCodeCredentialBuilder; use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, TokenCredential, TokenRequest, + AuthorizationCodeCredential, ConfidentialClientApplication, TokenCredentialExecutor, + TokenRequest, }; // Use a refresh token to get a new access token. @@ -11,7 +12,7 @@ async fn using_auth_code_credential( ) { credential.with_refresh_token(refresh_token); - let _response = credential.get_token_async().await; + let _response = credential.execute_async().await; } async fn using_confidential_client( @@ -21,7 +22,7 @@ async fn using_confidential_client( credential.with_refresh_token(refresh_token); let mut confidential_client = ConfidentialClientApplication::from(credential); - let _response = confidential_client.get_token_async().await; + let _response = confidential_client.execute_async().await; } async fn using_auth_code_credential_builder( @@ -32,5 +33,5 @@ async fn using_auth_code_credential_builder( .with_refresh_token(refresh_token) .build(); - let _response = credential.get_token_async().await; + let _response = credential.execute_async().await; } diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials.rs index 1d561860..2c432307 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials.rs @@ -10,8 +10,8 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, TokenCredential, - TokenRequest, + ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, + TokenCredentialExecutor, TokenRequest, }; // This example shows programmatically getting an access token using the client credentials @@ -28,10 +28,21 @@ pub async fn get_token_silent() { ConfidentialClientApplication::from(client_secret_credential); let response = confidential_client_application - .get_token_async() + .execute_async() .await .unwrap(); println!("{response:#?}"); let body: MsalTokenResponse = response.json().await.unwrap(); } + +pub async fn get_token_silent2() { + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_client_secret_credential(CLIENT_SECRET) + .build(); + + let response = confidential_client.execute_async().await.unwrap(); + println!("{response:#?}"); + + let body: MsalTokenResponse = response.json().await.unwrap(); +} diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 1d89c46a..9d2078c6 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,4 +1,4 @@ -use graph_oauth::identity::{DeviceCodeCredential, TokenCredential}; +use graph_oauth::identity::{DeviceCodeCredential, TokenCredentialExecutor}; use graph_rs_sdk::oauth::{MsalTokenResponse, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; @@ -14,141 +14,9 @@ fn get_oauth() -> OAuthSerializer { .client_id(client_id) .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/devicecode") .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") + .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token") .add_scope("files.read") .add_scope("offline_access"); oauth } - -fn device_code_credential() -> DeviceCodeCredential { - let client_id = "CLIENT_ID"; - - DeviceCodeCredential::builder() - .with_scope(vec!["files.read", "offline_access"]) - .with_client_id(client_id) - .build() -} - -// When polling to wait on the user to enter a device code you should check the errors -// so that you know what to do next. -// -// authorization_pending: The user hasn't finished authenticating, but hasn't canceled the flow. Repeat the request after at least interval seconds. -// authorization_declined: The end user denied the authorization request. Stop polling and revert to an unauthenticated state. -// bad_verification_code: The device_code sent to the /token endpoint wasn't recognized. Verify that the client is sending the correct device_code in the request. -// expired_token: Value of expires_in has been exceeded and authentication is no longer possible with device_code. Stop polling and revert to an unauthenticated state. -async fn poll_for_access_token( - device_code: &str, - interval: u64, - message: &str, -) -> GraphResult<serde_json::Value> { - let mut credential = device_code_credential(); - - let mut oauth = get_oauth(); - oauth.device_code(device_code); - - let mut request = oauth.build_async().device_code(); - let response = request.access_token().send().await?; - - println!("{response:#?}"); - - let status = response.status(); - - let body: serde_json::Value = response.json().await?; - println!("{body:#?}"); - - if !status.is_success() { - loop { - // Wait the amount of seconds that interval is. - std::thread::sleep(Duration::from_secs(interval)); - - let response = request.access_token().send().await?; - - let status = response.status(); - println!("{response:#?}"); - - let body: serde_json::Value = response.json().await?; - println!("{body:#?}"); - - if status.is_success() { - return Ok(body); - } else { - let option_error = body["error"].as_str(); - - if let Some(error) = option_error { - match error { - "authorization_pending" => println!("Still waiting on user to sign in"), - "authorization_declined" => panic!("user declined to sign in"), - "bad_verification_code" => { - println!("Bad verification code. Message:\n{message:#?}") - } - "expired_token" => panic!("token has expired - user did not sign in"), - _ => { - panic!("This isn't the error we expected: {error:#?}"); - } - } - } else { - // Body should have error or we should bail. - panic!("Crap hit the fan"); - } - } - } - } - - Ok(body) -} - -// The authorization url for device code must be https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode -// where tenant can be common, -pub async fn device_code() -> GraphResult<()> { - /* - let mut credential = device_code_credential(); - let response = credential.get_token_async().await?; - - println!("{:#?}", response); - let json: serde_json::Value = response.json().await?; - println!("{:#?}", json); - - let device_code = json["device_code"].as_str().unwrap(); - let interval = json["interval"].as_u64().unwrap(); - let message = json["message"].as_str().unwrap(); - */ - - /* - The authorization request is a POST and a successful response body will look similar to: - - Object { - "device_code": String("FABABAAEAAAD--DLA3VO7QrddgJg7WevrgJ7Czy_TDsDClt2ELoEC8ePWFs"), - "expires_in": Number(900), - "interval": Number(5), - "message": String("To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code FQK5HW3UF to authenticate."), - "user_code": String("FQK5HW3UF"), - "verification_uri": String("https://microsoft.com/devicelogin"), - } - */ - - /* - // Print the message to the user who needs to sign in: - println!("{message:#?}"); - - // Poll for the response to the token endpoint. This will go through once - // the user has entered the code and signed in. - let access_token_json = poll_for_access_token(device_code, interval, message).await?; - let access_token: MsalTokenResponse = serde_json::from_value(access_token_json)?; - println!("{access_token:#?}"); - */ - - /* - // Get a refresh token. First pass the access token to the oauth instance. - oauth.access_token(access_token); - let mut handler = oauth.build_async().device_code(); - - let response = handler.refresh_token().send().await?; - println!("{response:#?}"); - - let body: serde_json::Value = response.json().await?; - println!("{body:#?}"); - - */ - Ok(()) -} diff --git a/examples/oauth/environment_credential.rs b/examples/oauth/environment_credential.rs index cbdc3d7a..473dfaea 100644 --- a/examples/oauth/environment_credential.rs +++ b/examples/oauth/environment_credential.rs @@ -13,7 +13,7 @@ use std::env::VarError; // "AZURE_USERNAME" (Required) // "AZURE_PASSWORD" (Required) pub fn username_password() -> Result<(), VarError> { - let public_client_application = EnvironmentCredential::resource_owner_password_credential()?; + let public_client = EnvironmentCredential::resource_owner_password_credential()?; Ok(()) } @@ -22,6 +22,6 @@ pub fn username_password() -> Result<(), VarError> { // "AZURE_CLIENT_ID" (Required) // "AZURE_CLIENT_SECRET" (Required) pub fn client_secret_credential() -> Result<(), VarError> { - let confidential_client_application = EnvironmentCredential::client_secret_credential()?; + let confidential_client = EnvironmentCredential::client_secret_credential()?; Ok(()) } diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth/implicit_grant.rs index dee2c4ad..a43c3b6b 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth/implicit_grant.rs @@ -1,10 +1,15 @@ use std::collections::BTreeSet; + +// NOTICE: The Implicit Flow is considered legacy and cannot be used in a +// ConfidentialClientApplication or Public + // The following example shows authenticating an application to use the OneDrive REST API // for a native client. Native clients typically use the implicit OAuth flow. This requires // using the browser to log in. To get an access token, set the response type to 'token' // which will return an access token in the URL. The implicit flow does not make POST requests // for access tokens like other flows do. // + // There are two versions of the implicit flow. The first, called token flow is used // for Microsoft V1.0 OneDrive authentication. The second is Microsoft's implementation // of the OAuth V2.0 implicit flow. @@ -18,20 +23,20 @@ use std::collections::BTreeSet; // // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 use graph_rs_sdk::oauth::{ - ImplicitCredential, Prompt, ResponseMode, ResponseType, TokenCredential, + ImplicitCredential, Prompt, ResponseMode, ResponseType, TokenCredentialExecutor, }; fn oauth_implicit_flow() { let authorizer = ImplicitCredential::builder() .with_client_id("<YOUR_CLIENT_ID>") - .with_redirect_uri("http://localhost:8000/redirect") .with_prompt(Prompt::Login) .with_response_type(ResponseType::Token) .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url = authorizer.url().unwrap(); @@ -47,7 +52,7 @@ fn oauth_implicit_flow() { fn multi_response_types() { let _ = ImplicitCredential::builder() .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) - .build(); + .build_credential(); // Or @@ -56,5 +61,5 @@ fn multi_response_types() { "token".to_string(), "id_token".to_string(), ]))) - .build(); + .build_credential(); } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index e9063914..be008331 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -32,7 +32,7 @@ use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, MsalTokenResponse, ProofKeyForCodeExchange, PublicClientApplication, - TokenCredential, TokenRequest, + TokenCredentialExecutor, TokenRequest, }; #[tokio::main] @@ -64,18 +64,17 @@ async fn main() { async fn auth_code_grant(authorization_code: &str) { let pkce = ProofKeyForCodeExchange::generate().unwrap(); - let credential = AuthorizationCodeCredential::builder() - .with_authorization_code(authorization_code) + let credential = AuthorizationCodeCredential::builder(authorization_code) .with_client_id("CLIENT_ID") .with_client_secret("CLIENT_SECRET") .with_redirect_uri("http://localhost:8000/redirect") .unwrap() - .with_proof_key_for_code_exchange(&pkce) + .with_pkce(&pkce) .build(); let mut confidential_client = ConfidentialClientApplication::from(credential); - let response = confidential_client.get_token_async().await.unwrap(); + let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); let access_token: MsalTokenResponse = response.json().await.unwrap(); @@ -87,7 +86,7 @@ async fn client_credentials() { let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); let mut confidential_client = ConfidentialClientApplication::from(client_secret_credential); - let response = confidential_client.get_token_async().await.unwrap(); + let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); let access_token: MsalTokenResponse = response.json().await.unwrap(); diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index f27c1266..e515036c 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,4 +1,6 @@ -use graph_oauth::identity::{ResponseType, TokenCredential, TokenRequest}; +use graph_oauth::identity::{ + ConfidentialClientApplication, ResponseType, TokenCredentialExecutor, TokenRequest, +}; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{IdToken, MsalTokenResponse, OAuthSerializer}; use url::Url; @@ -26,7 +28,7 @@ fn open_id_credential( authorization_code: &str, client_id: &str, client_secret: &str, -) -> anyhow::Result<OpenIdCredential> { +) -> anyhow::Result<ConfidentialClientApplication> { Ok(OpenIdCredential::builder() .with_authorization_code(authorization_code) .with_client_id(client_id) @@ -59,7 +61,7 @@ async fn handle_redirect( let code = id_token.code.clone(); let mut credential = open_id_credential(code.as_ref(), CLIENT_ID, CLIENT_SECRET).unwrap(); - let mut result = credential.get_token_async().await; + let mut result = credential.execute_async().await; dbg!(&result); @@ -98,7 +100,6 @@ pub async fn start_server_main() { .and_then(handle_redirect); std::env::set_var("RUST_LOG", "trace"); - std::env::set_var("GRAPH_TEST_ENV", "true"); let url = open_id_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); webbrowser::open(url.as_ref()); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 18323440..54d4bec8 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -5,7 +5,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, ConfidentialClientApplication, MsalTokenResponse, PKey, - TokenCredential, X509Certificate, X509, + TokenCredentialExecutor, X509Certificate, X509, }; use std::fs::File; use std::io::Read; @@ -88,11 +88,10 @@ pub fn get_confidential_client( let x509_certificate = X509Certificate::new_with_tenant(client_id, tenant_id, cert, pkey); - let credentials = AuthorizationCodeCertificateCredential::builder() - .with_authorization_code(authorization_code) + let credentials = AuthorizationCodeCertificateCredential::builder(authorization_code) .with_client_id(client_id) .with_tenant(tenant_id) - .with_certificate(&x509_certificate)? + .with_x509(&x509_certificate)? .with_scope(vec!["User.Read"]) .with_redirect_uri("http://localhost:8080")? .build(); @@ -116,7 +115,7 @@ async fn handle_redirect( get_confidential_client(access_code.code.as_str(), CLIENT_ID, TENANT).unwrap(); // Returns reqwest::Response - let response = confidential_client.get_token_async().await.unwrap(); + let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); if response.status().is_success() { diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 43c8fe77..875c58a2 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -22,6 +22,7 @@ base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" hex = "0.4.3" +http = "0.2.9" openssl = { version = "0.10", optional=true } reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index c4dbbfd0..68a78c6a 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -24,7 +24,7 @@ pub enum OAuthParameter { ClientId, ClientSecret, AuthorizationUrl, - AccessTokenUrl, + TokenUrl, RefreshTokenUrl, RedirectUri, AuthorizationCode, @@ -61,7 +61,7 @@ impl OAuthParameter { OAuthParameter::ClientId => "client_id", OAuthParameter::ClientSecret => "client_secret", OAuthParameter::AuthorizationUrl => "authorization_url", - OAuthParameter::AccessTokenUrl => "access_token_url", + OAuthParameter::TokenUrl => "access_token_url", OAuthParameter::RefreshTokenUrl => "refresh_token_url", OAuthParameter::RedirectUri => "redirect_uri", OAuthParameter::AuthorizationCode => "code", @@ -172,9 +172,8 @@ impl OAuthSerializer { pub fn insert<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut OAuthSerializer { let v = value.to_string(); match oac { - OAuthParameter::RefreshTokenUrl - | OAuthParameter::PostLogoutRedirectURI - | OAuthParameter::AccessTokenUrl + OAuthParameter::PostLogoutRedirectURI + | OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl | OAuthParameter::LogoutURL => { Url::parse(v.as_ref()) @@ -203,9 +202,8 @@ impl OAuthSerializer { pub fn entry_with<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { let v = value.to_string(); match oac { - OAuthParameter::RefreshTokenUrl - | OAuthParameter::PostLogoutRedirectURI - | OAuthParameter::AccessTokenUrl + OAuthParameter::PostLogoutRedirectURI + | OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl | OAuthParameter::LogoutURL => { Url::parse(v.as_ref()) @@ -337,10 +335,10 @@ impl OAuthSerializer { /// ``` /// # use graph_oauth::oauth::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); - /// oauth.access_token_url("https://example.com/token"); + /// oauth.token_uri("https://example.com/token"); /// ``` - pub fn access_token_url(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::AccessTokenUrl, value) + pub fn token_uri(&mut self, value: &str) -> &mut OAuthSerializer { + self.insert(OAuthParameter::TokenUrl, value) } /// Set the refresh token url of a request for OAuth @@ -368,9 +366,7 @@ impl OAuthSerializer { let token_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/token",); let auth_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/authorize",); - self.authorization_url(&auth_url) - .access_token_url(&token_url) - .refresh_token_url(&token_url) + self.authorization_url(&auth_url).token_uri(&token_url) } /// Set the authorization, access token, and refresh token URL @@ -387,10 +383,6 @@ impl OAuthSerializer { host: &AzureCloudInstance, authority: &Authority, ) -> &mut OAuthSerializer { - if host.eq(&AzureCloudInstance::OneDriveAndSharePoint) { - return self.legacy_authority(); - } - let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); let auth_url = format!( "{}/{}/oauth2/v2.0/authorize", @@ -398,9 +390,7 @@ impl OAuthSerializer { authority.as_ref() ); - self.authorization_url(&auth_url) - .access_token_url(&token_url) - .refresh_token_url(&token_url) + self.authorization_url(&auth_url).token_uri(&token_url) } pub fn authority_admin_consent( @@ -411,15 +401,13 @@ impl OAuthSerializer { let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); let auth_url = format!("{}/{}/adminconsent", host.as_ref(), authority.as_ref()); - self.authorization_url(&auth_url) - .access_token_url(&token_url) - .refresh_token_url(&token_url) + self.authorization_url(&auth_url).token_uri(&token_url) } pub fn legacy_authority(&mut self) -> &mut OAuthSerializer { - self.authorization_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()); - self.access_token_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()); - self.refresh_token_url(AzureCloudInstance::OneDriveAndSharePoint.as_ref()) + let url = "https://login.live.com/oauth20_desktop.srf".to_string(); + self.authorization_url(url.as_str()); + self.token_uri(url.as_str()) } /// Set the redirect url of a request @@ -1935,7 +1923,7 @@ impl DeviceCodeGrant { pub fn access_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2052,7 +2040,7 @@ impl AsyncDeviceCodeGrant { pub fn access_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2213,7 +2201,7 @@ impl AccessTokenGrant { pub fn access_token(&mut self) -> AccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); @@ -2377,7 +2365,7 @@ impl AsyncAccessTokenGrant { pub fn access_token(&mut self) -> AsyncAccessTokenRequest { self.oauth .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::AccessTokenUrl); + let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); let params = self .oauth .params(self.grant.available_credentials(GrantRequest::AccessToken)); diff --git a/graph-oauth/src/discovery/graph_discovery.rs b/graph-oauth/src/discovery/graph_discovery.rs index 2c5e7bf8..1fc7bba2 100644 --- a/graph-oauth/src/discovery/graph_discovery.rs +++ b/graph-oauth/src/discovery/graph_discovery.rs @@ -129,7 +129,7 @@ impl GraphDiscovery { let k: MicrosoftSigningKeysV1 = self.signing_keys()?; oauth .authorization_url(k.authorization_endpoint.as_str()) - .access_token_url(k.token_endpoint.as_str()) + .token_uri(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); Ok(oauth) @@ -138,7 +138,7 @@ impl GraphDiscovery { let k: MicrosoftSigningKeysV2 = self.signing_keys()?; oauth .authorization_url(k.authorization_endpoint.as_str()) - .access_token_url(k.token_endpoint.as_str()) + .token_uri(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); Ok(oauth) @@ -163,7 +163,7 @@ impl GraphDiscovery { let k: MicrosoftSigningKeysV1 = self.async_signing_keys().await?; oauth .authorization_url(k.authorization_endpoint.as_str()) - .access_token_url(k.token_endpoint.as_str()) + .token_uri(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); Ok(oauth) @@ -172,7 +172,7 @@ impl GraphDiscovery { let k: MicrosoftSigningKeysV2 = self.async_signing_keys().await?; oauth .authorization_url(k.authorization_endpoint.as_str()) - .access_token_url(k.token_endpoint.as_str()) + .token_uri(k.token_endpoint.as_str()) .refresh_token_url(k.token_endpoint.as_str()) .logout_url(k.end_session_endpoint.as_str()); Ok(oauth) diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 5e8e1d39..8d35b044 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -1,60 +1,3 @@ -/* - /// <summary> - /// Client ID (also known as App ID) of the application as registered in the - /// application registration portal (https://aka.ms/msal-net-register-app) - /// </summary> - public string ClientId { get; set; } - - /// <summary> - /// Tenant from which the application will allow users to sign it. This can be: - /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). - /// This property is mutually exclusive with <see cref="AadAuthorityAudience"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - /// <remarks>The name of the property was chosen to ensure compatibility with AzureAdOptions - /// in ASP.NET Core configuration files (even the semantics would be tenant)</remarks> - public string TenantId { get; set; } - - /// <summary> - /// Sign-in audience. This property is mutually exclusive with TenantId. If both - /// are provided, an exception will be thrown. - /// </summary> - public AadAuthorityAudience AadAuthorityAudience { get; set; } = AadAuthorityAudience.None; - - /// <summary> - /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). - /// The name was chosen to ensure compatibility with AzureAdOptions in ASP.NET Core. - /// This property is mutually exclusive with <see cref="AzureCloudInstance"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - public string Instance { get; set; } - - /// <summary> - /// Specific instance in the case of Azure Active Directory. - /// It allows users to use the enum instead of the explicit URL. - /// This property is mutually exclusive with <see cref="Instance"/>. If both - /// are provided, an exception will be thrown. - /// </summary> - public AzureCloudInstance AzureCloudInstance { get; set; } = AzureCloudInstance.None; - - /// <summary> - /// This redirect URI needs to be registered in the app registration. See https://aka.ms/msal-net-register-app for - /// details on which redirect URIs are defined by default by MSAL.NET and how to register them. - /// Also use: <see cref="PublicClientApplicationBuilder.WithDefaultRedirectUri"/> which provides - /// a good default for public client applications for all platforms. - /// - /// For web apps and web APIs, the redirect URI is computed from the URL where the application is running - /// (for instance, <c>baseUrl//signin-oidc</c> for ASP.NET Core web apps). - /// - /// For daemon applications (confidential client applications using only the Client Credential flow - /// that is calling <c>AcquireTokenForClient</c>), no reply URI is needed. - /// </summary> - /// <remarks>This is especially important when you deploy an application that you have initially tested locally; - /// you then need to add the reply URL of the deployed application in the application registration portal - /// </remarks> - public string RedirectUri { get; set; } -*/ - use crate::identity::AadAuthorityAudience; use crate::oauth::AzureCloudInstance; use url::Url; @@ -65,17 +8,44 @@ pub struct ApplicationOptions { /// Client ID (also known as App ID) of the application as registered in the /// application registration portal (https://aka.ms/msal-net-register-app) /// Required parameter for ApplicationOptions. - #[serde(alias = "clientId", alias = "ClientId")] + #[serde(alias = "clientId", alias = "ClientId", alias = "client_id")] pub client_id: String, /// Tenant from which the application will allow users to sign it. This can be: /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). /// This property is mutually exclusive with [AadAuthorityAudience]. If both - /// are provided, an error will be thrown. - #[serde(alias = "tenantId", alias = "TenantId")] + /// are provided, an error result will be returned when mapping to [crate::identity::ConfidentialClientApplication] + #[serde(alias = "tenantId", alias = "TenantId", alias = "tenant_id")] pub tenant_id: Option<String>, + #[serde( + alias = "aadAuthorityAudience", + alias = "AadAuthorityAudience", + alias = "aad_authority_audience" + )] pub aad_authority_audience: Option<AadAuthorityAudience>, #[serde(alias = "instance", alias = "Instance")] pub instance: Option<Url>, + #[serde( + alias = "azureCloudInstance", + alias = "AzureCloudInstance", + alias = "azure_cloud_instance" + )] pub azure_cloud_instance: Option<AzureCloudInstance>, + #[serde(alias = "redirectUri", alias = "RedirectUri", alias = "redirect_uri")] pub redirect_uri: Option<Url>, } + +#[cfg(test)] +mod test { + use super::*; + use std::fs::File; + + #[test] + fn application_options_from_file() { + let file = File::open(r#"test_files\application_options\aad_options.json"#).unwrap(); + let application_options: ApplicationOptions = serde_json::from_reader(file).unwrap(); + assert_eq!( + application_options.aad_authority_audience, + Some(AadAuthorityAudience::PersonalMicrosoftAccount) + ); + } +} diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 327e1b91..3fc1be5b 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -17,8 +17,6 @@ pub enum AzureCloudInstance { AzureGermany, /// US Government cloud. Maps to https://login.microsoftonline.us AzureUsGovernment, - /// Legacy OneDrive and SharePoint. Maps to "https://login.live.com/oauth20_desktop.srf" - OneDriveAndSharePoint, } impl AsRef<str> for AzureCloudInstance { @@ -28,9 +26,6 @@ impl AsRef<str> for AzureCloudInstance { AzureCloudInstance::AzureChina => "https://login.chinacloudapi.cn", AzureCloudInstance::AzureGermany => "https://login.microsoftonline.de", AzureCloudInstance::AzureUsGovernment => "https://login.microsoftonline.us", - AzureCloudInstance::OneDriveAndSharePoint => { - "https://login.live.com/oauth20_desktop.srf" - } } } } @@ -56,7 +51,6 @@ impl AzureCloudInstance { AzureCloudInstance::AzureUsGovernment => { "https://management.usgovcloudapi.net/.default" } - AzureCloudInstance::OneDriveAndSharePoint => "", } } } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs new file mode 100644 index 00000000..dd1812db --- /dev/null +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -0,0 +1,61 @@ +use crate::identity::credentials::application_builder::AuthorityHost; +use crate::identity::Authority; +use reqwest::header::HeaderMap; +use std::collections::HashMap; +use url::Url; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AppConfig { + pub(crate) tenant_id: Option<String>, + /// Required. + /// The Application (client) ID that the Azure portal - App registrations page assigned + /// to your app + pub(crate) client_id: String, + pub(crate) authority: Authority, + pub(crate) authority_url: AuthorityHost, + pub(crate) extra_query_parameters: HashMap<String, String>, + pub(crate) extra_header_parameters: HeaderMap, + /// Optional - Some flows may require the redirect URI + /// The redirect_uri of your app, where authentication responses can be sent and received + /// by your app. It must exactly match one of the redirect_uris you registered in the portal, + /// except it must be URL-encoded. + pub(crate) redirect_uri: Option<Url>, +} + +impl AppConfig { + pub fn new() -> AppConfig { + AppConfig { + tenant_id: None, + client_id: String::with_capacity(32), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + } + } + + pub(crate) fn new_with_client_id(client_id: impl AsRef<str>) -> AppConfig { + AppConfig { + tenant_id: None, + client_id: client_id.as_ref().to_string(), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + } + } + + pub(crate) fn init(tenant_id: impl AsRef<str>, client_id: impl AsRef<str>) -> AppConfig { + AppConfig { + tenant_id: Some(tenant_id.as_ref().to_string()), + client_id: client_id.as_ref().to_string(), + authority: Authority::TenantId(tenant_id.as_ref().to_string()), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + } + } +} diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index a9f2b46a..fbe2a3a5 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,10 +1,103 @@ +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::client_assertion_credential::ClientAssertionCredentialBuilder; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; -use crate::identity::{application_options::ApplicationOptions, Authority, AzureCloudInstance}; +use crate::identity::{ + application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, + AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredential, + AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCertificateCredential, + ClientCertificateCredentialBuilder, ClientCredentialsAuthorizationUrlBuilder, + ClientSecretCredentialBuilder, +}; +use crate::oauth::ConfidentialClientApplication; use reqwest::header::HeaderMap; use std::collections::HashMap; use url::Url; +macro_rules! application_builder_impl { + ($name:ident) => { + impl $name { + pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + self.client_id = client_id.as_ref().to_owned(); + self + } + + pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + self.tenant_id = Some(tenant_id.as_ref().to_owned()); + self.authority = Authority::TenantId(tenant_id.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<AuthorityHost>, U: Into<Authority>>( + &mut self, + authority_host: T, + authority: U, + ) -> &mut Self { + self.authority_url = authority_host.into(); + self.authority = authority.into(); + self + } + + /// Adds a known Azure AD authority to the application to sign-in users specifying + /// the full authority Uri. See https://aka.ms/msal-net-application-configuration. + pub fn with_authority_uri(&mut self, authority_uri: Url) -> &mut Self { + self.authority_url = AuthorityHost::Uri(authority_uri); + self + } + + pub fn with_azure_cloud_instance( + &mut self, + azure_cloud_instance: AzureCloudInstance, + ) -> &mut Self { + self.authority_url = AuthorityHost::AzureCloudInstance(azure_cloud_instance); + self + } + + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.redirect_uri = Some(redirect_uri); + self.default_redirect_uri = false; + self + } + + pub fn with_default_redirect_uri(&mut self) -> &mut Self { + self.default_redirect_uri = true; + self + } + + pub fn with_extra_query_parameters<F: Fn(&mut HashMap<String, String>)>( + &mut self, + f: F, + ) -> &mut Self { + f(&mut self.extra_query_parameters); + self + } + + pub fn with_extra_header_parameters<F: Fn(&mut HeaderMap)>( + &mut self, + f: F, + ) -> &mut Self { + f(&mut self.extra_header_parameters); + self + } + } + }; +} + +/* +pub fn with_extra_query_parameters( + &mut self, + query_parameters: HashMap<String, String>, + ) -> &mut Self { + self.extra_query_parameters = query_parameters; + self + } + + pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { + self.extra_header_parameters = header_parameters; + self + } + */ + #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). @@ -39,136 +132,214 @@ pub enum ClientCredentialParameter { } pub struct ConfidentialClientApplicationBuilder { - client_id: String, - tenant_id: Option<String>, - authority: Authority, - authority_url: AuthorityHost, - redirect_uri: Option<Url>, + app_config: AppConfig, default_redirect_uri: bool, + redirect_uri: Option<Url>, client_credential_parameter: Option<ClientCredentialParameter>, - extra_query_parameters: HashMap<String, String>, - extra_header_parameters: HeaderMap, } +// application_builder_impl!(ConfidentialClientApplicationBuilder); + impl ConfidentialClientApplicationBuilder { - pub fn create(client_id: &str) -> ConfidentialClientApplicationBuilder { + pub fn new(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { ConfidentialClientApplicationBuilder { - client_id: client_id.to_owned(), - tenant_id: None, - authority: Default::default(), - authority_url: Default::default(), - redirect_uri: None, + app_config: AppConfig::new_with_client_id(client_id), default_redirect_uri: false, + redirect_uri: None, client_credential_parameter: None, - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), } } - pub fn create_with_application_options( + pub fn new_with_application_options( application_options: ApplicationOptions, ) -> anyhow::Result<ConfidentialClientApplicationBuilder> { ConfidentialClientApplicationBuilder::try_from(application_options) } - pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - self.client_id = client_id.as_ref().to_owned(); - self + pub fn get_authorization_request_url<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scopes: I, + ) -> AuthCodeAuthorizationUrlParameterBuilder { + let mut builder = AuthCodeAuthorizationUrlParameterBuilder::new(); + builder.with_scope(scopes); + builder } - pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { - self.tenant_id = Some(tenant_id.as_ref().to_owned()); - self.authority = Authority::TenantId(tenant_id.as_ref().to_owned()); - self + pub fn get_client_credential_request_url( + &mut self, + ) -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new() } - pub fn with_authority<T: Into<AuthorityHost>, U: Into<Authority>>( - &mut self, - authority_host: T, - authority: U, - ) -> &mut Self { - self.authority_url = authority_host.into(); - self.authority = authority.into(); - self + #[cfg(feature = "openssl")] + pub fn with_client_certificate_credential( + self, + certificate: &X509Certificate, + ) -> anyhow::Result<ClientCertificateCredentialBuilder> { + ClientCertificateCredentialBuilder::new_with_certificate(certificate, self.app_config) } - /// Adds a known Azure AD authority to the application to sign-in users specifying - /// the full authority Uri. See https://aka.ms/msal-net-application-configuration. - pub fn with_authority_uri(&mut self, authority_uri: Url) -> &mut Self { - self.authority_url = AuthorityHost::Uri(authority_uri); - self + pub fn with_client_secret_credential( + self, + client_secret: impl AsRef<str>, + ) -> ClientSecretCredentialBuilder { + ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) } - pub fn with_azure_cloud_instance( - &mut self, - azure_cloud_instance: AzureCloudInstance, - ) -> &mut Self { - self.authority_url = AuthorityHost::AzureCloudInstance(azure_cloud_instance); - self + pub fn with_client_assertion_credential( + self, + signed_assertion: impl AsRef<str>, + ) -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder::new_with_signed_assertion( + signed_assertion.as_ref().to_string(), + self.app_config, + ) } - pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { - self.redirect_uri = Some(redirect_uri); - self.default_redirect_uri = false; - self + pub fn with_authorization_code_credential( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new_with_auth_code(self.into(), authorization_code) } - pub fn with_default_redirect_uri(&mut self) -> &mut Self { - self.default_redirect_uri = true; - self + pub fn with_authorization_code_assertion_credential( + self, + authorization_code: impl AsRef<str>, + assertion: impl AsRef<str>, + ) -> AuthorizationCodeCertificateCredentialBuilder { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_assertion( + self.into(), + authorization_code, + assertion, + ) } #[cfg(feature = "openssl")] - pub fn with_certificate(&mut self, certificate: X509Certificate) -> &mut Self { - self.client_credential_parameter = - Some(ClientCredentialParameter::Certificate(certificate)); - self + pub fn with_authorization_code_certificate_credential( + self, + authorization_code: impl AsRef<str>, + x509: &X509Certificate, + ) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + self.into(), + authorization_code, + x509, + ) } +} - pub fn with_client_secret(&mut self, client_secret: impl AsRef<str>) -> &mut Self { - self.client_credential_parameter = Some(ClientCredentialParameter::SecretString( - client_secret.as_ref().to_owned(), - )); - self +impl From<ConfidentialClientApplicationBuilder> for AppConfig { + fn from(value: ConfidentialClientApplicationBuilder) -> Self { + value.app_config } +} + +impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { + type Error = anyhow::Error; + + fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { + anyhow::ensure!(!value.client_id.is_empty(), "Client id cannot be empty"); + anyhow::ensure!( + !(value.instance.is_some() && value.azure_cloud_instance.is_some()), + "Instance and AzureCloudInstance both specify the azure cloud instance and cannot be set at the same time" + ); + anyhow::ensure!( + !(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), + "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time" + ); + + /* + client_id: value.client_id, + tenant_id: value.tenant_id, + authority: value + .aad_authority_audience + .map(Authority::from) + .unwrap_or_default(), + authority_url: value + .azure_cloud_instance + .map(AuthorityHost::AzureCloudInstance) + .unwrap_or_default(), + */ - pub fn with_signed_assertion(&mut self, signed_assertion: impl AsRef<str>) -> &mut Self { - self.client_credential_parameter = Some(ClientCredentialParameter::SignedAssertion( - signed_assertion.as_ref().to_owned(), - )); - self + Ok(ConfidentialClientApplicationBuilder { + app_config: AppConfig { + tenant_id: value.tenant_id, + client_id: value.client_id, + authority: value + .aad_authority_audience + .map(Authority::from) + .unwrap_or_default(), + authority_url: value + .azure_cloud_instance + .map(AuthorityHost::AzureCloudInstance) + .unwrap_or_default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + }, + default_redirect_uri: value.redirect_uri.is_none(), + redirect_uri: value.redirect_uri, + client_credential_parameter: None, + }) } +} - pub fn with_extra_query_parameters( - &mut self, - query_parameters: HashMap<String, String>, - ) -> &mut Self { - self.extra_query_parameters = query_parameters; - self +pub struct ConfidentialClientAppSelectionBuilder { + builder: ConfidentialClientApplicationBuilder, +} + +impl ConfidentialClientAppSelectionBuilder {} + +pub struct PublicClientApplicationBuilder { + client_id: String, + tenant_id: Option<String>, + authority: Authority, + authority_url: AuthorityHost, + redirect_uri: Option<Url>, + default_redirect_uri: bool, + extra_query_parameters: HashMap<String, String>, + extra_header_parameters: HeaderMap, +} + +application_builder_impl!(PublicClientApplicationBuilder); + +impl PublicClientApplicationBuilder { + pub fn new(client_id: &str) -> PublicClientApplicationBuilder { + PublicClientApplicationBuilder { + client_id: client_id.to_owned(), + tenant_id: None, + authority: Default::default(), + authority_url: Default::default(), + default_redirect_uri: false, + redirect_uri: None, + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + } } - pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { - self.extra_header_parameters = header_parameters; - self + pub fn create_with_application_options( + application_options: ApplicationOptions, + ) -> anyhow::Result<PublicClientApplicationBuilder> { + PublicClientApplicationBuilder::try_from(application_options) } } -impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { +impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { type Error = anyhow::Error; fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { - anyhow::ensure!(value.client_id.is_empty(), "Client id cannot be empty"); + anyhow::ensure!(!value.client_id.is_empty(), "Client id cannot be empty"); anyhow::ensure!( !(value.instance.is_some() && value.azure_cloud_instance.is_some()), - "Instance and AzureCloudInstance cannot both be set" + "Instance and AzureCloudInstance both specify the azure cloud instance and cannot be set at the same time" ); anyhow::ensure!( !(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), - "TenantId and AadAuthorityAudience cannot both be set" + "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time" ); - let default_redirect_uri = value.redirect_uri.is_none(); - Ok(ConfidentialClientApplicationBuilder { + Ok(PublicClientApplicationBuilder { client_id: value.client_id, tenant_id: value.tenant_id, authority: value @@ -179,9 +350,8 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { .azure_cloud_instance .map(AuthorityHost::AzureCloudInstance) .unwrap_or_default(), + default_redirect_uri: value.redirect_uri.is_none(), redirect_uri: value.redirect_uri, - default_redirect_uri, - client_credential_parameter: None, extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), }) @@ -192,10 +362,12 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { mod test { use super::*; use crate::oauth::AadAuthorityAudience; + use reqwest::header::AUTHORIZATION; + use wry::http::HeaderValue; #[test] #[should_panic] - fn error_result_on_instance_and_aci() { + fn confidential_client_error_result_on_instance_and_aci() { ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { client_id: "client-id".to_string(), tenant_id: None, @@ -209,7 +381,7 @@ mod test { #[test] #[should_panic] - fn error_result_on_tenant_id_and_aad_audience() { + fn confidential_client_error_result_on_tenant_id_and_aad_audience() { ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { client_id: "client-id".to_owned(), tenant_id: Some("tenant_id".to_owned()), @@ -220,4 +392,47 @@ mod test { }) .unwrap(); } + + #[test] + #[should_panic] + fn public_client_error_result_on_instance_and_aci() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }) + .unwrap(); + } + + #[test] + #[should_panic] + fn public_client_error_result_on_tenant_id_and_aad_audience() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } + + /* + #[test] + fn extra_parameters() { + let mut confidential_client = ConfidentialClientApplicationBuilder::new("client-id"); + confidential_client.with_extra_query_parameters(|query| { + query.insert("name".into(), "123".into()); + }) + .with_extra_header_parameters(|map| { + map.insert(AUTHORIZATION, HeaderValue::from_static("Bearer Token")); + }); + assert_eq!(confidential_client.extra_header_parameters.get(AUTHORIZATION).unwrap(), &HeaderValue::from_static("Bearer Token")); + assert_eq!(confidential_client.extra_query_parameters.get("name").unwrap(), &String::from("123")); + } + */ } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 28696dd0..2ea6c1ea 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,7 +1,9 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AzureCloudInstance, TokenCredential, TokenCredentialOptions, CLIENT_ASSERTION_TYPE, + AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, + TokenCredentialOptions, CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; @@ -12,9 +14,9 @@ use url::Url; #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; -credential_builder_impl!( +credential_builder!( AuthorizationCodeCertificateCredentialBuilder, - AuthorizationCodeCertificateCredential + ConfidentialClientApplication ); /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application @@ -25,20 +27,12 @@ credential_builder_impl!( /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow #[derive(Clone, Debug)] pub struct AuthorizationCodeCertificateCredential { + pub(crate) app_config: AppConfig, /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. pub(crate) authorization_code: Option<String>, /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, - /// Optional - /// The redirect_uri of your app, where authentication responses can be sent and received - /// by your app. It must exactly match one of the redirect_uris you registered in the portal, - /// except it must be URL-encoded. - pub(crate) redirect_uri: Option<Url>, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. @@ -56,9 +50,6 @@ pub struct AuthorizationCodeCertificateCredential { /// to additional user data. You may also include other scopes in this request for requesting /// consent to various resources, if an access token is requested. pub(crate) scope: Vec<String>, - /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) authority: Authority, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } @@ -77,23 +68,35 @@ impl AuthorizationCodeCertificateCredential { } }; + let app_config = AppConfig { + client_id: client_id.as_ref().to_owned(), + tenant_id: None, + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: redirect_uri.clone(), + }; + Ok(AuthorizationCodeCertificateCredential { + app_config, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, - client_id: client_id.as_ref().to_owned(), - redirect_uri, code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), scope: vec![], - authority: Default::default(), - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }) } - pub fn builder() -> AuthorizationCodeCertificateCredentialBuilder { - AuthorizationCodeCertificateCredentialBuilder::new() + pub fn builder( + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCertificateCredentialBuilder { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code( + Default::default(), + authorization_code, + ) } pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { @@ -102,20 +105,21 @@ impl AuthorizationCodeCertificateCredential { } #[async_trait] -impl TokenCredential for AuthorizationCodeCertificateCredential { +impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.authority()); let uri = self .serializer - .get(OAuthParameter::AccessTokenUrl) - .ok_or(AF::msg_internal_err("access_token_url"))?; + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_internal_err("token_url"))?; Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.is_empty() { return AF::result(OAuthParameter::ClientId); } @@ -128,12 +132,12 @@ impl TokenCredential for AuthorizationCodeCertificateCredential { } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .extend_scopes(self.scope.clone()); - if let Some(redirect_uri) = self.redirect_uri.as_ref() { + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { self.serializer.redirect_uri(redirect_uri.as_str()); } @@ -199,11 +203,15 @@ impl TokenCredential for AuthorizationCodeCertificateCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id + } + + fn app_config(&self) -> &AppConfig { + &self.app_config } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() } } @@ -216,21 +224,78 @@ impl AuthorizationCodeCertificateCredentialBuilder { fn new() -> AuthorizationCodeCertificateCredentialBuilder { Self { credential: AuthorizationCodeCertificateCredential { + app_config: Default::default(), authorization_code: None, refresh_token: None, - client_id: String::with_capacity(32), - redirect_uri: None, code_verifier: None, - client_assertion_type: String::new(), - client_assertion: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + scope: vec![], + serializer: OAuthSerializer::new(), + }, + } + } + + pub(crate) fn new_with_auth_code( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCertificateCredentialBuilder { + Self { + credential: AuthorizationCodeCertificateCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + scope: vec![], + serializer: OAuthSerializer::new(), + }, + } + } + + pub(crate) fn new_with_auth_code_and_assertion( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + assertion: impl AsRef<str>, + ) -> AuthorizationCodeCertificateCredentialBuilder { + Self { + credential: AuthorizationCodeCertificateCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: assertion.as_ref().to_owned(), scope: vec![], - authority: Default::default(), - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }, } } + #[cfg(feature = "openssl")] + pub(crate) fn new_with_auth_code_and_x509( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + x509: &X509Certificate, + ) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { + let mut builder = Self { + credential: AuthorizationCodeCertificateCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + scope: vec![], + serializer: OAuthSerializer::new(), + }, + }; + + builder.with_x509(x509)?; + Ok(builder) + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self @@ -243,7 +308,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { } pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { - self.credential.redirect_uri = Some(redirect_uri.into_url()?); + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); Ok(self) } @@ -253,11 +318,11 @@ impl AuthorizationCodeCertificateCredentialBuilder { } #[cfg(feature = "openssl")] - pub fn with_certificate( + pub fn with_x509( &mut self, certificate_assertion: &X509Certificate, ) -> anyhow::Result<&mut Self> { - if let Some(tenant_id) = self.credential.authority.tenant_id() { + if let Some(tenant_id) = self.credential.authority().tenant_id() { self.with_client_assertion( certificate_assertion.sign_with_tenant(Some(tenant_id.clone()))?, ); @@ -279,6 +344,10 @@ impl AuthorizationCodeCertificateCredentialBuilder { self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); self } + + pub fn credential(self) -> AuthorizationCodeCertificateCredential { + self.credential + } } impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCertificateCredentialBuilder { @@ -301,3 +370,11 @@ impl From<AuthorizationCodeCertificateCredential> AuthorizationCodeCertificateCredentialBuilder { credential } } } + +impl From<AuthorizationCodeCertificateCredentialBuilder> + for AuthorizationCodeCertificateCredential +{ + fn from(builder: AuthorizationCodeCertificateCredentialBuilder) -> Self { + builder.credential + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index b89478b6..1d5dd50c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,7 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AuthCodeAuthorizationUrlParameters, Authority, AzureCloudInstance, ProofKeyForCodeExchange, - TokenCredential, TokenCredentialOptions, + AuthCodeAuthorizationUrlParameters, Authority, AzureCloudInstance, + ConfidentialClientApplication, ProofKeyForCodeExchange, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; @@ -10,9 +11,9 @@ use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; -credential_builder_impl!( +credential_builder!( AuthorizationCodeCredentialBuilder, - AuthorizationCodeCredential + ConfidentialClientApplication ); /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application @@ -23,6 +24,7 @@ credential_builder_impl!( /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow #[derive(Clone)] pub struct AuthorizationCodeCredential { + app_config: AppConfig, /// Required unless requesting a refresh token /// The authorization code obtained from a call to authorize. /// The code should be obtained with all required scopes. @@ -31,10 +33,6 @@ pub struct AuthorizationCodeCredential { /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, /// Required /// The application secret that you created in the app registration portal for your app. /// Don't use the application secret in a native app or single page app because a @@ -53,46 +51,70 @@ pub struct AuthorizationCodeCredential { /// to the authorization code flow, intended to allow apps to declare the resource they want /// the token for during token redemption. pub(crate) scope: Vec<String>, - /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) authority: Authority, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } impl AuthorizationCodeCredential { pub fn new<T: AsRef<str>, U: IntoUrl>( + tenant_id: T, + client_id: T, + client_secret: T, + authorization_code: T, + ) -> AuthorizationResult<AuthorizationCodeCredential> { + Ok(AuthorizationCodeCredential { + app_config: AppConfig::init(tenant_id, client_id), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: client_secret.as_ref().to_owned(), + redirect_uri: Url::parse("http://localhost").expect("Internal Error - please report"), + scope: vec![], + code_verifier: None, + serializer: OAuthSerializer::new(), + }) + } + + pub fn new_with_redirect_uri<T: AsRef<str>, U: IntoUrl>( + tenant_id: T, client_id: T, client_secret: T, authorization_code: T, redirect_uri: U, ) -> AuthorizationResult<AuthorizationCodeCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); + let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; + + let app_config = AppConfig { + tenant_id: Some(tenant_id.as_ref().to_owned()), + client_id: client_id.as_ref().to_owned(), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: Some(redirect_uri.clone()), + }; Ok(AuthorizationCodeCredential { + app_config, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, - client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, + redirect_uri, scope: vec![], - authority: Default::default(), code_verifier: None, - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }) } pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { - self.authorization_code = None; self.refresh_token = Some(refresh_token.as_ref().to_owned()); } - pub fn builder() -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new() + pub fn builder(authorization_code: impl AsRef<str>) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::builder(authorization_code) } pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { @@ -100,29 +122,138 @@ impl AuthorizationCodeCredential { } } +#[derive(Clone)] +pub struct AuthorizationCodeCredentialBuilder { + credential: AuthorizationCodeCredential, +} + +impl AuthorizationCodeCredentialBuilder { + fn new() -> AuthorizationCodeCredentialBuilder { + Self { + credential: AuthorizationCodeCredential { + app_config: Default::default(), + authorization_code: None, + refresh_token: None, + client_secret: String::new(), + redirect_uri: Url::parse("http://localhost") + .expect("Internal Error - please report"), + scope: vec![], + code_verifier: None, + serializer: OAuthSerializer::new(), + }, + } + } + + fn builder(authorization_code: impl AsRef<str>) -> AuthorizationCodeCredentialBuilder { + Self { + credential: AuthorizationCodeCredential { + app_config: Default::default(), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: String::new(), + redirect_uri: Url::parse("http://localhost") + .expect("Internal Error - please report"), + scope: vec![], + code_verifier: None, + serializer: OAuthSerializer::new(), + }, + } + } + + pub(crate) fn new_with_auth_code( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCredentialBuilder { + let redirect_uri = app_config + .redirect_uri + .clone() + .unwrap_or(Url::parse("http://localhost").expect("Internal Error - please report")); + + Self { + credential: AuthorizationCodeCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: String::new(), + redirect_uri, + scope: vec![], + code_verifier: None, + serializer: OAuthSerializer::new(), + }, + } + } + + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); + self.credential.refresh_token = None; + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + /// Defaults to http://localhost + pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { + self.credential.redirect_uri = redirect_uri.into_url()?; + Ok(self) + } + + pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { + self.credential.client_secret = client_secret.as_ref().to_owned(); + self + } + + fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + self + } + + pub fn with_pkce( + &mut self, + proof_key_for_code_exchange: &ProofKeyForCodeExchange, + ) -> &mut Self { + self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); + self + } +} + +impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCredentialBuilder { + fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { + let mut builder = AuthorizationCodeCredentialBuilder::new(); + let _ = builder.with_redirect_uri(value.redirect_uri); + builder + .with_scope(value.scope) + .with_client_id(value.client_id) + .with_authority(value.authority); + + builder + } +} + +impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { + fn from(credential: AuthorizationCodeCredential) -> Self { + AuthorizationCodeCredentialBuilder { credential } + } +} + #[async_trait] -impl TokenCredential for AuthorizationCodeCredential { +impl TokenCredentialExecutor for AuthorizationCodeCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); - - if self.refresh_token.is_none() { - let uri = self - .serializer - .get(OAuthParameter::AccessTokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } else { - let uri = self - .serializer - .get(OAuthParameter::RefreshTokenUrl) - .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } + .authority(azure_authority_host, &self.authority()); + + let uri = self + .serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.client_id().clone(); + if client_id.trim().is_empty() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -131,7 +262,7 @@ impl TokenCredential for AuthorizationCodeCredential { } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) .extend_scopes(self.scope.clone()); @@ -193,95 +324,22 @@ impl TokenCredential for AuthorizationCodeCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() } fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) + Some(( + self.app_config.client_id.clone(), + self.client_secret.clone(), + )) } -} -#[derive(Clone)] -pub struct AuthorizationCodeCredentialBuilder { - credential: AuthorizationCodeCredential, -} - -impl AuthorizationCodeCredentialBuilder { - fn new() -> AuthorizationCodeCredentialBuilder { - Self { - credential: AuthorizationCodeCredential { - authorization_code: None, - refresh_token: None, - client_id: String::with_capacity(32), - client_secret: String::new(), - redirect_uri: Url::parse("http://localhost") - .expect("Internal Error - please report"), - scope: vec![], - authority: Default::default(), - code_verifier: None, - token_credential_options: TokenCredentialOptions::default(), - serializer: OAuthSerializer::new(), - }, - } - } - - pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { - self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); - self.credential.refresh_token = None; - self - } - - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.credential.authorization_code = None; - self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); - self - } - - /// Defaults to http://localhost - pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { - self.credential.redirect_uri = redirect_uri.into_url()?; - Ok(self) - } - - pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { - self.credential.client_secret = client_secret.as_ref().to_owned(); - self - } - - fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { - self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); - self - } - - pub fn with_proof_key_for_code_exchange( - &mut self, - proof_key_for_code_exchange: &ProofKeyForCodeExchange, - ) -> &mut Self { - self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); - self - } -} - -impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCredentialBuilder { - fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { - let mut builder = AuthorizationCodeCredentialBuilder::new(); - let _ = builder.with_redirect_uri(value.redirect_uri); - builder - .with_scope(value.scope) - .with_client_id(value.client_id) - .with_authority(value.authority); - - builder - } -} - -impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { - fn from(credential: AuthorizationCodeCredential) -> Self { - AuthorizationCodeCredentialBuilder { credential } + fn app_config(&self) -> &AppConfig { + &self.app_config } } @@ -291,26 +349,26 @@ mod test { #[test] fn with_tenant_id_common() { - let credential = AuthorizationCodeCredential::builder() + let credential = AuthorizationCodeCredential::builder("code") .with_authority(Authority::TenantId("common".into())) .build(); - assert_eq!(credential.authority, Authority::TenantId("common".into())) + assert_eq!(credential.authority(), Authority::TenantId("common".into())) } #[test] fn with_tenant_id_adfs() { - let credential = AuthorizationCodeCredential::builder() + let credential = AuthorizationCodeCredential::builder("code") .with_authority(Authority::AzureDirectoryFederatedServices) .build(); - assert_eq!(credential.authority.as_ref(), "adfs"); + assert_eq!(credential.authority().as_ref(), "adfs"); } #[test] #[should_panic] fn authorization_code_missing_required_value() { - let mut credential_builder = AuthorizationCodeCredential::builder(); + let mut credential_builder = AuthorizationCodeCredentialBuilder::new(); credential_builder .with_redirect_uri("https://localhost:8080") .unwrap() @@ -325,7 +383,7 @@ mod test { #[test] #[should_panic] fn required_value_missing_client_id() { - let mut credential_builder = AuthorizationCodeCredential::builder(); + let mut credential_builder = AuthorizationCodeCredential::builder("code"); credential_builder .with_authorization_code("code") .with_refresh_token("token"); @@ -335,7 +393,7 @@ mod test { #[test] fn serialization() { - let mut credential_builder = AuthorizationCodeCredential::builder(); + let mut credential_builder = AuthorizationCodeCredential::builder("code"); let mut credential = credential_builder .with_redirect_uri("https://localhost") .unwrap() diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs index 6d95ed53..1bba594f 100644 --- a/graph-oauth/src/identity/credentials/client_application.rs +++ b/graph-oauth/src/identity/credentials/client_application.rs @@ -1,9 +1,9 @@ -use crate::identity::{CredentialStoreType, TokenCredential}; +use crate::identity::{CredentialStoreType, TokenCredentialExecutor}; use crate::oauth::MsalTokenResponse; use async_trait::async_trait; #[async_trait] -pub trait ClientApplication: TokenCredential { +pub trait ClientApplication: TokenCredentialExecutor { fn get_credential_from_store(&self) -> &CredentialStoreType; fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType); @@ -14,7 +14,7 @@ pub trait ClientApplication: TokenCredential { if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { Ok(credential_from_store.clone()) } else { - let response = self.get_token()?; + let response = self.execute()?; let token_value: serde_json::Value = response.json()?; let bearer = token_value.to_string(); let access_token_result: serde_json::Result<MsalTokenResponse> = @@ -40,7 +40,7 @@ pub trait ClientApplication: TokenCredential { if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { Ok(credential_from_store.clone()) } else { - let response = self.get_token_async().await?; + let response = self.execute_async().await?; let token_value: serde_json::Value = response.json().await?; let bearer = token_value.to_string(); let access_token_result: serde_json::Result<MsalTokenResponse> = diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs new file mode 100644 index 00000000..01b31d14 --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -0,0 +1,177 @@ +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + Authority, AzureCloudInstance, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, +}; +use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; +use async_trait::async_trait; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use std::collections::HashMap; +use url::Url; + +credential_builder!( + ClientAssertionCredentialBuilder, + ConfidentialClientApplication +); + +#[derive(Clone)] +pub struct ClientAssertionCredential { + /// The client (application) ID of the service principal + pub(crate) app_config: AppConfig, + /// The value passed for the scope parameter in this request should be the resource + /// identifier (application ID URI) of the resource you want, affixed with the .default + /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// Default is https://graph.microsoft.com/.default. + pub(crate) scope: Vec<String>, + pub(crate) client_assertion_type: String, + pub(crate) client_assertion: String, + pub(crate) refresh_token: Option<String>, + serializer: OAuthSerializer, +} + +impl ClientAssertionCredential { + pub fn new( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + ) -> ClientAssertionCredential { + ClientAssertionCredential { + app_config: AppConfig::init(tenant_id, client_id), + scope: vec!["https://graph.microsoft.com/.default".into()], + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: Default::default(), + refresh_token: None, + serializer: Default::default(), + } + } +} + +pub struct ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential, +} + +impl ClientAssertionCredentialBuilder { + pub(crate) fn new() -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential { + app_config: Default::default(), + scope: vec!["https://graph.microsoft.com/.default".into()], + client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), + client_assertion: Default::default(), + refresh_token: None, + serializer: Default::default(), + }, + } + } + + pub(crate) fn new_with_signed_assertion( + signed_assertion: String, + app_config: AppConfig, + ) -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential { + app_config, + scope: vec!["https://graph.microsoft.com/.default".into()], + client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), + client_assertion: signed_assertion, + refresh_token: None, + serializer: Default::default(), + }, + } + } + + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + self.credential.client_assertion = client_assertion.as_ref().to_owned(); + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } +} + +#[async_trait] +impl TokenCredentialExecutor for ClientAssertionCredential { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.serializer + .authority(azure_authority_host, &self.authority()); + + let uri = self + .serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_err("token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) + } + + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + let client_id = self.client_id().clone(); + if client_id.trim().is_empty() { + return AF::result(OAuthParameter::ClientId.alias()); + } + + if self.client_assertion.trim().is_empty() { + return AF::result(OAuthParameter::ClientAssertion.alias()); + } + + if self.client_assertion_type.trim().is_empty() { + self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); + } + + self.serializer + .client_id(client_id.as_str()) + .client_assertion(self.client_assertion.as_str()) + .client_assertion_type(self.client_assertion_type.as_str()); + + if self.scope.is_empty() { + self.serializer + .add_scope("https://graph.microsoft.com/.default"); + } + + return if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AF::msg_result( + OAuthParameter::RefreshToken.alias(), + "refresh_token is set but is empty", + ); + } + + self.serializer + .refresh_token(refresh_token.as_ref()) + .grant_type("refresh_token"); + + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + OAuthParameter::RefreshToken, + ], + ) + } else { + self.serializer.grant_type("client_credentials"); + + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ) + }; + } + + fn client_id(&self) -> &String { + &self.app_config.client_id + } + + fn authority(&self) -> Authority { + self.app_config.authority.clone() + } + + fn app_config(&self) -> &AppConfig { + &self.app_config + } +} diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs new file mode 100644 index 00000000..284d6505 --- /dev/null +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -0,0 +1,98 @@ +use crate::identity::credentials::app_config::AppConfig; +macro_rules! credential_builder_impl { + ($name:ident, $credential:ty) => { + impl $name { + pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + if self.credential.client_id.is_empty() { + self.credential.client_id.push_str(client_id.as_ref()); + } else { + self.credential.client_id = client_id.as_ref().to_owned(); + } + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { + self.credential.authority = + crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<crate::identity::Authority>>( + &mut self, + authority: T, + ) -> &mut Self { + self.credential.authority = authority.into(); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scope: I, + ) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + /* + pub fn build(&self) -> $credential { + self.credential.clone() + } + */ + } + }; +} + +macro_rules! credential_builder_base { + ($name:ident) => { + impl $name { + pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { + if self.credential.app_config.client_id.is_empty() { + self.credential + .app_config + .client_id + .push_str(client_id.as_ref()); + } else { + self.credential.app_config.client_id = client_id.as_ref().to_owned(); + } + self + } + + /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] + pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + self.credential.app_config.authority = + crate::identity::Authority::TenantId(tenant_id.as_ref().to_owned()); + self.credential.app_config.tenant_id = Some(tenant_id.as_ref().to_owned()); + self + } + + pub fn with_authority<T: Into<crate::identity::Authority>>( + &mut self, + authority: T, + ) -> &mut Self { + self.credential.app_config.authority = authority.into(); + self + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scope: I, + ) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + } + }; +} + +macro_rules! credential_builder { + ($name:ident, $client:ty) => { + credential_builder_base!($name); + + impl $name { + pub fn build(&self) -> $client { + <$client>::new(self.credential.clone()) + } + } + }; +} diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 14a63e54..a738202f 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,55 +1,66 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, +}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use http_body_util::BodyExt; use std::collections::HashMap; use url::Url; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; -use crate::oauth::ClientCredentialsAuthorizationUrlBuilder; +use crate::oauth::{ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication}; pub(crate) static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; -credential_builder_impl!( +credential_builder!( ClientCertificateCredentialBuilder, - ClientCertificateCredential + ConfidentialClientApplication ); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials #[derive(Clone)] #[allow(dead_code)] pub struct ClientCertificateCredential { - /// The client (application) ID of the service principal - pub(crate) client_id: String, + pub(crate) app_config: AppConfig, /// The value passed for the scope parameter in this request should be the resource /// identifier (application ID URI) of the resource you want, affixed with the .default /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, - pub(crate) authority: Authority, pub(crate) client_assertion_type: String, pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } impl ClientCertificateCredential { pub fn new<T: AsRef<str>>(client_id: T, client_assertion: T) -> ClientCertificateCredential { ClientCertificateCredential { - client_id: client_id.as_ref().to_owned(), + app_config: AppConfig::new_with_client_id(client_id), scope: vec!["https://graph.microsoft.com/.default".into()], - authority: Default::default(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), refresh_token: None, - token_credential_options: Default::default(), serializer: Default::default(), } } + #[cfg(feature = "openssl")] + pub fn x509<T: AsRef<str>>( + client_id: T, + x509: &X509Certificate, + ) -> anyhow::Result<ClientCertificateCredential> { + let mut builder = ClientCertificateCredentialBuilder::new(); + builder + .with_client_id(client_id.as_ref().to_owned()) + .with_certificate(x509)?; + Ok(builder.credential) + } + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { self.refresh_token = Some(refresh_token.as_ref().to_owned()); self @@ -65,28 +76,21 @@ impl ClientCertificateCredential { } #[async_trait] -impl TokenCredential for ClientCertificateCredential { +impl TokenCredentialExecutor for ClientCertificateCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); - - if self.refresh_token.is_none() { - let uri = self - .serializer - .get(OAuthParameter::AccessTokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } else { - let uri = self - .serializer - .get(OAuthParameter::RefreshTokenUrl) - .ok_or(AF::msg_err("refresh_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } + .authority(azure_authority_host, &self.app_config.authority); + + let uri = self + .serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_err("token_url", "Internal Error"))?; + Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.client_id().clone(); + if client_id.trim().is_empty() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } @@ -99,7 +103,7 @@ impl TokenCredential for ClientCertificateCredential { } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()); @@ -146,11 +150,15 @@ impl TokenCredential for ClientCertificateCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() + } + + fn app_config(&self) -> &AppConfig { + &self.app_config } } @@ -162,18 +170,42 @@ impl ClientCertificateCredentialBuilder { fn new() -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { - client_id: String::with_capacity(32), + app_config: Default::default(), scope: vec!["https://graph.microsoft.com/.default".into()], - authority: Default::default(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), refresh_token: None, - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }, } } + fn builder<T: ToString, I: IntoIterator<Item = T>>( + scopes: I, + ) -> ClientCertificateCredentialBuilder { + ClientCertificateCredentialBuilder { + credential: ClientCertificateCredential { + app_config: Default::default(), + scope: scopes.into_iter().map(|s| s.to_string()).collect(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + refresh_token: None, + serializer: OAuthSerializer::new(), + }, + } + } + + #[cfg(feature = "openssl")] + pub(crate) fn new_with_certificate( + x509: &X509Certificate, + app_config: AppConfig, + ) -> anyhow::Result<ClientCertificateCredentialBuilder> { + let mut builder = ClientCertificateCredentialBuilder::new(); + builder.credential.app_config = app_config; + builder.with_certificate(x509)?; + Ok(builder) + } + #[cfg(feature = "openssl")] pub fn with_certificate(&mut self, certificate: &X509Certificate) -> anyhow::Result<&mut Self> { if let Some(tenant_id) = self.credential.authority.tenant_id() { @@ -193,6 +225,10 @@ impl ClientCertificateCredentialBuilder { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } + + pub(crate) fn credential(self) -> ClientCertificateCredential { + self.credential + } } impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder { @@ -201,6 +237,12 @@ impl From<ClientCertificateCredential> for ClientCertificateCredentialBuilder { } } +impl From<ClientCertificateCredentialBuilder> for ClientCertificateCredential { + fn from(builder: ClientCertificateCredentialBuilder) -> Self { + builder.credential + } +} + #[cfg(test)] mod test { use super::*; @@ -211,15 +253,15 @@ mod test { fn credential_builder() { let mut builder = ClientCertificateCredentialBuilder::new(); builder.with_client_id(TEST_CLIENT_ID); - assert_eq!(builder.credential.client_id, TEST_CLIENT_ID); + assert_eq!(builder.credential.app_config.client_id, TEST_CLIENT_ID); builder.with_client_id("123"); - assert_eq!(builder.credential.client_id, "123"); + assert_eq!(builder.credential.app_config.client_id, "123"); - builder.credential.client_id = "".into(); - assert!(builder.credential.client_id.is_empty()); + builder.credential.app_config.client_id = "".into(); + assert!(builder.credential.app_config.client_id.is_empty()); builder.with_client_id(TEST_CLIENT_ID); - assert_eq!(builder.credential.client_id, TEST_CLIENT_ID); + assert_eq!(builder.credential.app_config.client_id, TEST_CLIENT_ID); } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index d519f1df..7734bcc9 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,6 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, TokenCredential, + Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, + ConfidentialClientApplication, TokenCredentialExecutor, }; use crate::oauth::TokenCredentialOptions; use async_trait::async_trait; @@ -8,7 +10,7 @@ use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; use url::Url; -credential_builder_impl!(ClientSecretCredentialBuilder, ClientSecretCredential); +credential_builder!(ClientSecretCredentialBuilder, ConfidentialClientApplication); /// Client Credentials flow using a client secret. /// @@ -22,10 +24,7 @@ credential_builder_impl!(ClientSecretCredentialBuilder, ClientSecretCredential); /// See [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) #[derive(Clone, Debug)] pub struct ClientSecretCredential { - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, + pub(crate) app_config: AppConfig, /// Required /// The application secret that you created in the app registration portal for your app. /// Don't use the application secret in a native app or single page app because a @@ -41,19 +40,15 @@ pub struct ClientSecretCredential { /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, - pub(crate) authority: Authority, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } impl ClientSecretCredential { pub fn new<T: AsRef<str>>(client_id: T, client_secret: T) -> ClientSecretCredential { ClientSecretCredential { - client_id: client_id.as_ref().to_owned(), + app_config: AppConfig::new_with_client_id(client_id), client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], - authority: Default::default(), - token_credential_options: Default::default(), serializer: OAuthSerializer::new(), } } @@ -64,15 +59,17 @@ impl ClientSecretCredential { client_secret: T, ) -> ClientSecretCredential { ClientSecretCredential { - client_id: client_id.as_ref().to_owned(), + app_config: AppConfig::init(tenant_id, client_id), client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], - authority: Authority::TenantId(tenant_id.as_ref().to_owned()), - token_credential_options: Default::default(), serializer: OAuthSerializer::new(), } } + pub(crate) fn create() -> ClientSecretCredentialBuilder { + ClientSecretCredentialBuilder::new() + } + pub fn builder() -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new() } @@ -83,19 +80,24 @@ impl ClientSecretCredential { } #[async_trait] -impl TokenCredential for ClientSecretCredential { +impl TokenCredentialExecutor for ClientSecretCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.authority()); - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( - AuthorizationFailure::msg_err("access_token_url", "Internal Error"), - )?; + let uri = + self.serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AuthorizationFailure::msg_err( + "access_token_url", + "Internal Error", + ))?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.client_id().clone(); + if client_id.trim().is_empty() { return AuthorizationFailure::result(OAuthParameter::ClientId); } @@ -117,15 +119,22 @@ impl TokenCredential for ClientSecretCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() } fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) + Some(( + self.app_config.client_id.clone(), + self.client_secret.clone(), + )) + } + + fn app_config(&self) -> &AppConfig { + &self.app_config } } @@ -137,11 +146,44 @@ impl ClientSecretCredentialBuilder { fn new() -> Self { Self { credential: ClientSecretCredential { - client_id: String::new(), + app_config: Default::default(), client_secret: String::new(), scope: vec!["https://graph.microsoft.com/.default".into()], - authority: Default::default(), - token_credential_options: Default::default(), + serializer: Default::default(), + }, + } + } + + fn builder<T: ToString, I: IntoIterator<Item = T>>(scopes: I) -> ClientSecretCredentialBuilder { + let provided_scopes: Vec<String> = scopes.into_iter().map(|s| s.to_string()).collect(); + let scope = { + if provided_scopes.is_empty() { + vec!["https://graph.microsoft.com/.default".into()] + } else { + provided_scopes + } + }; + + Self { + credential: ClientSecretCredential { + app_config: Default::default(), + client_secret: String::new(), + scope, + serializer: Default::default(), + }, + } + } + + pub(crate) fn new_with_client_secret( + client_secret: impl AsRef<str>, + app_config: AppConfig, + ) -> ClientSecretCredentialBuilder { + println!("{:#?}", &app_config); + Self { + credential: ClientSecretCredential { + app_config, + client_secret: client_secret.as_ref().to_string(), + scope: vec!["https://graph.microsoft.com/.default".into()], serializer: Default::default(), }, } @@ -151,6 +193,10 @@ impl ClientSecretCredentialBuilder { self.credential.client_secret = client_secret.as_ref().to_owned(); self } + + pub fn credential(self) -> ClientSecretCredential { + self.credential + } } impl Default for ClientSecretCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 0cfcf57f..ee4fbc05 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,12 +1,24 @@ +use crate::identity::application_options::ApplicationOptions; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::application_builder::{ + ClientCredentialParameter, ConfidentialClientApplicationBuilder, +}; +use crate::identity::credentials::client_assertion_credential::ClientAssertionCredential; use crate::identity::{ - AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AzureCloudInstance, - ClientApplication, ClientCertificateCredential, ClientSecretCredential, CredentialStore, - CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, TokenCacheProviderType, - TokenCredential, TokenCredentialOptions, + AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredential, + AuthorizationCodeCredential, AzureCloudInstance, ClientApplication, + ClientCertificateCredential, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredential, + CredentialStore, CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, + TokenCacheProviderType, TokenCredentialExecutor, TokenCredentialOptions, +}; +use crate::oauth::{ + AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, + ClientCertificateCredentialBuilder, ClientSecretCredentialBuilder, + UnInitializedCredentialStore, }; -use crate::oauth::UnInitializedCredentialStore; use async_trait::async_trait; use graph_error::{AuthorizationResult, GraphResult}; +use http_body_util::BodyExt; use reqwest::header::{HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; @@ -14,32 +26,45 @@ use std::collections::HashMap; use url::Url; use wry::http::HeaderMap; +pub(crate) struct CredentialExecutor<T: TokenCredentialExecutor + Send>(T); + /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. pub struct ConfidentialClientApplication { http_client: reqwest::Client, - token_credential_options: TokenCredentialOptions, - credential: Box<dyn TokenCredential + Send>, - credential_store: Box<dyn CredentialStore + Send>, + credential: Box<dyn TokenCredentialExecutor + Send>, } impl ConfidentialClientApplication { - pub fn new<T>( - credential: T, - options: TokenCredentialOptions, - ) -> GraphResult<ConfidentialClientApplication> + pub(crate) fn new<T>(credential: T) -> ConfidentialClientApplication where T: Into<ConfidentialClientApplication>, { - let mut confidential_client = credential.into(); - confidential_client.token_credential_options = options; - Ok(confidential_client) + credential.into() + } + + pub(crate) fn credential<T>(credential: T) -> ConfidentialClientApplication + where + T: TokenCredentialExecutor + Send + 'static, + { + ConfidentialClientApplication { + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), + credential: Box::new(credential), + } + } + + pub fn builder(client_id: &str) -> ConfidentialClientApplicationBuilder { + ConfidentialClientApplicationBuilder::new(client_id) } } #[async_trait] -impl TokenCredential for ConfidentialClientApplication { +impl TokenCredentialExecutor for ConfidentialClientApplication { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } @@ -52,13 +77,25 @@ impl TokenCredential for ConfidentialClientApplication { self.credential.client_id() } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.credential.authority() + } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.credential.azure_cloud_instance() + } + + fn basic_auth(&self) -> Option<(String, String)> { + self.credential.basic_auth() + } + + fn app_config(&self) -> &AppConfig { + self.credential.app_config() } - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host; - let uri = self.credential.uri(&azure_authority_host)?; + fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.credential.uri(&azure_cloud_instance)?; let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -84,9 +121,9 @@ impl TokenCredential for ConfidentialClientApplication { } } - async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host; - let uri = self.credential.uri(&azure_authority_host)?; + async fn execute_async(&mut self) -> anyhow::Result<Response> { + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.credential.uri(&azure_cloud_instance)?; let form = self.credential.form_urlencode()?; let basic_auth = self.credential.basic_auth(); let mut headers = HeaderMap::new(); @@ -117,106 +154,39 @@ impl TokenCredential for ConfidentialClientApplication { } } -impl ClientApplication for ConfidentialClientApplication { - fn get_credential_from_store(&self) -> &CredentialStoreType { - match self.credential_store.token_cache_provider() { - TokenCacheProviderType::UnInitialized => &CredentialStoreType::UnInitialized, - TokenCacheProviderType::InMemory => { - let client_id = self.client_id(); - self.credential_store - .get_token_by_client_id(client_id.as_str()) - } - TokenCacheProviderType::Session => &CredentialStoreType::UnInitialized, - TokenCacheProviderType::Distributed => &CredentialStoreType::UnInitialized, - } - } - - fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType) { - match self.credential_store.token_cache_provider() { - TokenCacheProviderType::UnInitialized => {} - TokenCacheProviderType::InMemory => { - let client_id = self.client_id().clone(); - self.credential_store - .update_by_client_id(client_id.as_str(), credential_store_type); - } - TokenCacheProviderType::Session => {} - TokenCacheProviderType::Distributed => {} - } - } -} - impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCredential) -> Self { - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - credential_store: Box::new(UnInitializedCredentialStore), - } + ConfidentialClientApplication::credential(value) } } impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplication { fn from(value: AuthorizationCodeCertificateCredential) -> Self { - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - credential_store: Box::new(UnInitializedCredentialStore), - } + ConfidentialClientApplication::credential(value) } } impl From<ClientSecretCredential> for ConfidentialClientApplication { fn from(value: ClientSecretCredential) -> Self { - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - credential_store: Box::new(InMemoryCredentialStore::new()), - } + ConfidentialClientApplication::credential(value) } } impl From<ClientCertificateCredential> for ConfidentialClientApplication { fn from(value: ClientCertificateCredential) -> Self { - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - credential_store: Box::new(UnInitializedCredentialStore), - } + ConfidentialClientApplication::credential(value) + } +} + +impl From<ClientAssertionCredential> for ConfidentialClientApplication { + fn from(value: ClientAssertionCredential) -> Self { + ConfidentialClientApplication::credential(value) } } impl From<OpenIdCredential> for ConfidentialClientApplication { fn from(value: OpenIdCredential) -> Self { - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - credential_store: Box::new(UnInitializedCredentialStore), - } + ConfidentialClientApplication::credential(value) } } @@ -227,8 +197,7 @@ mod test { #[test] fn confidential_client_new() { - let credential = AuthorizationCodeCredential::builder() - .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") .with_scope(vec!["Read.Write", "Fall.Down"]) @@ -236,9 +205,7 @@ mod test { .unwrap() .build(); - let mut confidential_client = - ConfidentialClientApplication::new(credential, TokenCredentialOptions::default()) - .unwrap(); + let mut confidential_client = ConfidentialClientApplication::from(credential); let credential_uri = confidential_client .credential .uri(&AzureCloudInstance::AzurePublic) @@ -252,18 +219,14 @@ mod test { #[test] fn confidential_client_tenant() { - let credential = AuthorizationCodeCredential::builder() - .with_authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_client_id("bb301aaa-1201-4259-a230923fds32") .with_client_secret("CLDIE3F") - .with_scope(vec!["Read.Write", "Fall.Down"]) .with_redirect_uri("http://localhost:8888/redirect") .unwrap() .with_authority(Authority::Consumers) .build(); - let mut confidential_client = - ConfidentialClientApplication::new(credential, TokenCredentialOptions::default()) - .unwrap(); + let mut confidential_client = ConfidentialClientApplication::from(credential); let credential_uri = confidential_client .credential .uri(&AzureCloudInstance::AzurePublic) diff --git a/graph-oauth/src/identity/credentials/credential_builder.rs b/graph-oauth/src/identity/credentials/credential_builder.rs deleted file mode 100644 index 3c8c5366..00000000 --- a/graph-oauth/src/identity/credentials/credential_builder.rs +++ /dev/null @@ -1,49 +0,0 @@ -macro_rules! credential_builder_impl { - ($name:ident, $credential:ty) => { - impl $name { - pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - if self.credential.client_id.is_empty() { - self.credential.client_id.push_str(client_id.as_ref()); - } else { - self.credential.client_id = client_id.as_ref().to_owned(); - } - self - } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { - self.credential.authority = - crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<crate::identity::Authority>>( - &mut self, - authority: T, - ) -> &mut Self { - self.credential.authority = authority.into(); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( - &mut self, - scope: I, - ) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_token_credential_options( - &mut self, - options: crate::identity::TokenCredentialOptions, - ) -> &mut Self { - self.credential.token_credential_options = options; - self - } - - pub fn build(&self) -> $credential { - self.credential.clone() - } - } - }; -} diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index e73cbf9a..539f984a 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,15 +1,18 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; -use crate::oauth::DeviceCode; +use crate::identity::{ + Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, +}; +use crate::oauth::{DeviceCode, PublicClientApplication}; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; +use crate::identity::credentials::app_config::AppConfig; use url::Url; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; -credential_builder_impl!(DeviceCodeCredentialBuilder, DeviceCodeCredential); +credential_builder!(DeviceCodeCredentialBuilder, PublicClientApplication); /// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, /// or a printer. To enable this flow, the device has the user visit a webpage in a browser on @@ -18,15 +21,12 @@ credential_builder_impl!(DeviceCodeCredentialBuilder, DeviceCodeCredential); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code #[derive(Clone)] pub struct DeviceCodeCredential { + pub(crate) app_config: AppConfig, /// Required when requesting a new access token using a refresh token /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, - /// Required. /// The device_code returned in the device authorization request. /// A device_code is a long string used to verify the session between the client and the authorization server. /// The client uses this parameter to request the access token from the authorization server. @@ -37,9 +37,6 @@ pub struct DeviceCodeCredential { /// to the authorization code flow, intended to allow apps to declare the resource they want /// the token for during token redemption. pub(crate) scope: Vec<String>, - /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) authority: Authority, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } @@ -50,12 +47,10 @@ impl DeviceCodeCredential { scope: I, ) -> DeviceCodeCredential { DeviceCodeCredential { + app_config: AppConfig::new_with_client_id(client_id), refresh_token: None, - client_id: client_id.as_ref().to_owned(), device_code: Some(device_code.as_ref().to_owned()), scope: scope.into_iter().map(|s| s.to_string()).collect(), - authority: Default::default(), - token_credential_options: Default::default(), serializer: Default::default(), } } @@ -154,15 +149,15 @@ impl DeviceCodeCredential { } } -impl TokenCredential for DeviceCodeCredential { +impl TokenCredentialExecutor for DeviceCodeCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.app_config.authority); if self.refresh_token.is_none() { let uri = self .serializer - .get(OAuthParameter::AccessTokenUrl) + .get(OAuthParameter::TokenUrl) .ok_or(AF::msg_internal_err("access_token_url"))?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } else { @@ -175,12 +170,13 @@ impl TokenCredential for DeviceCodeCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.is_empty() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .extend_scopes(self.scope.clone()); if let Some(refresh_token) = self.refresh_token.as_ref() { @@ -240,11 +236,15 @@ impl TokenCredential for DeviceCodeCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id + } + + fn authority(&self) -> Authority { + self.app_config.authority.clone() } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn app_config(&self) -> &AppConfig { + &self.app_config } } @@ -257,12 +257,10 @@ impl DeviceCodeCredentialBuilder { fn new() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { credential: DeviceCodeCredential { + app_config: Default::default(), refresh_token: None, - client_id: String::with_capacity(32), device_code: None, scope: vec![], - authority: Default::default(), - token_credential_options: Default::default(), serializer: Default::default(), }, } @@ -279,18 +277,22 @@ impl DeviceCodeCredentialBuilder { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } + + /* + pub fn build(&self) -> PublicClientApplication { + PublicClientApplication::from(self.credential.clone()) + } + */ } impl From<&DeviceCode> for DeviceCodeCredentialBuilder { fn from(value: &DeviceCode) -> Self { DeviceCodeCredentialBuilder { credential: DeviceCodeCredential { + app_config: AppConfig::new(), refresh_token: None, - client_id: String::new(), device_code: Some(value.device_code.clone()), scope: vec![], - authority: Default::default(), - token_credential_options: Default::default(), serializer: Default::default(), }, } diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 0312a93b..b9ffd3be 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -1,5 +1,7 @@ +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AuthorizationSerializer, AzureCloudInstance, ClientSecretCredential, TokenCredential, + Authority, AuthorizationSerializer, AzureCloudInstance, ClientSecretCredential, + TokenCredentialExecutor, }; use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, @@ -16,7 +18,7 @@ const AZURE_USERNAME: &str = "AZURE_USERNAME"; const AZURE_PASSWORD: &str = "AZURE_PASSWORD"; pub struct EnvironmentCredential { - pub credential: Box<dyn TokenCredential + Send>, + pub credential: Box<dyn TokenCredentialExecutor + Send>, } impl EnvironmentCredential { @@ -65,14 +67,10 @@ impl EnvironmentCredential { azure_client_id, azure_client_secret, ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), + )), None => Ok(ConfidentialClientApplication::new( ClientSecretCredential::new(azure_client_id, azure_client_secret), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), + )), } } @@ -81,12 +79,12 @@ impl EnvironmentCredential { let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_username = option_env!("AZURE_USERNAME").ok_or(VarError::NotPresent)?; let azure_password = option_env!("AZURE_PASSWORD").ok_or(VarError::NotPresent)?; - EnvironmentCredential::username_password_env( + Ok(EnvironmentCredential::username_password_env( tenant_id.map(|s| s.to_owned()), azure_client_id.to_owned(), azure_username.to_owned(), azure_password.to_owned(), - ) + )) } fn try_username_password_runtime_env() -> Result<PublicClientApplication, VarError> { @@ -94,12 +92,12 @@ impl EnvironmentCredential { let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_username = std::env::var(AZURE_USERNAME)?; let azure_password = std::env::var(AZURE_PASSWORD)?; - EnvironmentCredential::username_password_env( + Ok(EnvironmentCredential::username_password_env( tenant_id, azure_client_id, azure_username, azure_password, - ) + )) } fn username_password_env( @@ -107,27 +105,21 @@ impl EnvironmentCredential { azure_client_id: String, azure_username: String, azure_password: String, - ) -> Result<PublicClientApplication, VarError> { + ) -> PublicClientApplication { match tenant_id { - Some(tenant_id) => Ok(PublicClientApplication::new( - ResourceOwnerPasswordCredential::new_with_tenant( + Some(tenant_id) => { + PublicClientApplication::new(ResourceOwnerPasswordCredential::new_with_tenant( tenant_id, azure_client_id, azure_username, azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), - None => Ok(PublicClientApplication::new( - ResourceOwnerPasswordCredential::new( - azure_client_id, - azure_username, - azure_password, - ), - Default::default(), - ) - .map_err(|_| VarError::NotPresent)?), + )) + } + None => PublicClientApplication::new(ResourceOwnerPasswordCredential::new( + azure_client_id, + azure_username, + azure_password, + )), } } } @@ -142,6 +134,28 @@ impl AuthorizationSerializer for EnvironmentCredential { } } +impl TokenCredentialExecutor for EnvironmentCredential { + fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.credential.uri(azure_authority_host) + } + + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + self.credential.form_urlencode() + } + + fn client_id(&self) -> &String { + self.credential.client_id() + } + + fn authority(&self) -> Authority { + self.credential.authority() + } + + fn app_config(&self) -> &AppConfig { + self.credential.app_config() + } +} + impl From<ClientSecretCredential> for EnvironmentCredential { fn from(value: ClientSecretCredential) -> Self { EnvironmentCredential { diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index e3ad10d5..1539c26c 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,11 +1,13 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; use crate::oauth::TokenCredentialOptions; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; -credential_builder_impl!(ImplicitCredentialBuilder, ImplicitCredential); +credential_builder_base!(ImplicitCredentialBuilder); /// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens) /// are returned directly from the /authorize endpoint instead of the /token endpoint. This is @@ -14,10 +16,7 @@ credential_builder_impl!(ImplicitCredentialBuilder, ImplicitCredential); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow #[derive(Clone)] pub struct ImplicitCredential { - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, + pub(crate) app_config: AppConfig, /// Required /// If not set, defaults to code /// Must include id_token for OpenID Connect sign-in. It may also include the response_type @@ -28,13 +27,6 @@ pub struct ImplicitCredential { /// also contain code in place of token to provide an authorization code, for use in the /// authorization code flow. This id_token+code response is sometimes called the hybrid flow. pub(crate) response_type: Vec<ResponseType>, - /// Optional - /// The redirect_uri of your app, where authentication responses can be sent and received - /// by your app. It must exactly match one of the redirect_uris you registered in the portal, - /// except it must be URL-encoded. - /// - /// URL-encoding is done for you in the sdk client. - pub(crate) redirect_uri: Option<String>, /// Required /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the /// scope openid, which translates to the "Sign you in" permission in the consent UI. @@ -84,11 +76,6 @@ pub struct ImplicitCredential { /// for that tenant. This hint prevents guests from signing into this application, and limits /// the use of cloud credentials like FIDO. pub(crate) domain_hint: Option<String>, - /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) authority: Authority, - /// [ImplicitCredential] does not use TokenCredentialOptions. - /// This is here for compatibility with the [CredentialBuilder] trait. - token_credential_options: TokenCredentialOptions, } impl ImplicitCredential { @@ -98,9 +85,8 @@ impl ImplicitCredential { scope: I, ) -> ImplicitCredential { ImplicitCredential { - client_id: client_id.as_ref().to_owned(), + app_config: AppConfig::new_with_client_id(client_id), response_type: vec![ResponseType::Token], - redirect_uri: None, scope: scope.into_iter().map(|s| s.to_string()).collect(), response_mode: ResponseMode::Query, state: None, @@ -108,8 +94,6 @@ impl ImplicitCredential { prompt: None, login_hint: None, domain_hint: None, - authority: Default::default(), - token_credential_options: Default::default(), } } @@ -126,8 +110,8 @@ impl ImplicitCredential { azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.trim().is_empty() { return AuthorizationFailure::result("client_id"); } @@ -136,10 +120,10 @@ impl ImplicitCredential { } serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .nonce(self.nonce.as_str()) .extend_scopes(self.scope.clone()) - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.app_config.authority); let response_types: Vec<String> = self.response_type.iter().map(|s| s.to_string()).collect(); @@ -247,9 +231,8 @@ impl ImplicitCredentialBuilder { pub fn new() -> ImplicitCredentialBuilder { ImplicitCredentialBuilder { credential: ImplicitCredential { - client_id: String::with_capacity(32), + app_config: Default::default(), response_type: vec![ResponseType::Code], - redirect_uri: None, scope: vec![], response_mode: ResponseMode::Query, state: None, @@ -257,15 +240,13 @@ impl ImplicitCredentialBuilder { prompt: None, login_hint: None, domain_hint: None, - authority: Default::default(), - token_credential_options: Default::default(), }, } } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.credential.redirect_uri = Some(redirect_uri.as_ref().to_owned()); - self + pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); + Ok(self) } /// Default is code. Must include code for the authorization code flow. @@ -352,6 +333,10 @@ impl ImplicitCredentialBuilder { pub fn url(&self) -> AuthorizationResult<Url> { self.credential.url() } + + pub fn build_credential(&self) -> ImplicitCredential { + self.credential.clone() + } } #[cfg(test)] @@ -360,17 +345,19 @@ mod test { #[test] fn serialize_uri() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_response_mode(ResponseMode::Fragment) .with_state("12345") .with_nonce("678910") .with_prompt(Prompt::None) .with_login_hint("myuser@mycompany.com") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -378,14 +365,16 @@ mod test { #[test] fn set_open_id_fragment() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -396,13 +385,15 @@ mod test { #[test] fn set_open_id_fragment2() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -413,13 +404,15 @@ mod test { #[test] fn response_type_join() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -430,7 +423,8 @@ mod test { #[test] fn response_type_join_string() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::StringSet( vec!["id_token".to_owned(), "token".to_owned()] @@ -438,9 +432,10 @@ mod test { .collect(), )) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -451,13 +446,15 @@ mod test { #[test] fn response_type_into_iter() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::IdToken) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -468,13 +465,15 @@ mod test { #[test] fn response_type_into_iter2() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build(); + .build_credential(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -486,12 +485,14 @@ mod test { #[test] #[should_panic] fn missing_scope_panic() { - let authorizer = ImplicitCredential::builder() + let mut authorizer = ImplicitCredential::builder(); + authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https::/localhost:8080/myapp") + .unwrap() .with_nonce("678910") - .build(); + .build_credential(); let _ = authorizer.url().unwrap(); } @@ -500,6 +501,7 @@ mod test { fn generate_nonce() { let url = ImplicitCredential::builder() .with_redirect_uri("https::/localhost:8080") + .unwrap() .with_client_id("client_id") .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs index b536235c..594eef9f 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs @@ -60,18 +60,11 @@ impl CodeFlowCredential { impl AuthorizationSerializer for CodeFlowCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { - if azure_authority_host.ne(&AzureCloudInstance::OneDriveAndSharePoint) { - return AuthorizationFailure::msg_result( - "uri", - "Code flow can only be used with AzureCloudInstance::OneDriveAndSharePoint", - ); - } - self.serializer .authority(azure_authority_host, &Authority::Common); if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthParameter::AccessTokenUrl).ok_or( + let uri = self.serializer.get(OAuthParameter::TokenUrl).ok_or( AuthorizationFailure::msg_err("access_token_url", "Internal Error"), )?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index d4565fa8..22dbca1c 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,14 +1,16 @@ #[macro_use] -mod credential_builder; +mod client_builder_impl; pub mod legacy; +mod app_config; mod application_builder; mod as_query; mod auth_code_authorization_url_parameters; mod authorization_code_certificate_credential; mod authorization_code_credential; mod client_application; +mod client_assertion_credential; mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; @@ -27,7 +29,7 @@ mod public_client_application_builder; mod resource_owner_password_credential; mod response_mode; mod response_type; -mod token_credential; +mod token_credential_executor; mod token_credential_options; mod token_request; @@ -39,11 +41,11 @@ pub use auth_code_authorization_url_parameters::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use client_application::*; +pub use client_builder_impl::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; pub use confidential_client_application::*; -pub use credential_builder::*; pub(crate) use crypto::*; pub use device_code_credential::*; pub use display::*; @@ -58,7 +60,7 @@ pub use public_client_application_builder::*; pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; -pub use token_credential::*; +pub use token_credential_executor::*; pub use token_credential_options::*; pub use token_request::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 39540aa8..6db92d90 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -349,10 +349,10 @@ impl OpenIdAuthorizationUrlBuilder { /// /// Supported response types are: /// - /// code - /// id_token - /// code id_token - /// id_token token + /// - code + /// - id_token + /// - code id_token + /// - id_token token pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( &mut self, response_type: I, diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index afd7a50d..e26e2159 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,16 +1,17 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, OpenIdAuthorizationUrl, ProofKeyForCodeExchange, - TokenCredential, TokenCredentialOptions, + TokenCredentialExecutor, }; -use crate::oauth::OpenIdAuthorizationUrlBuilder; +use crate::oauth::{ConfidentialClientApplication, OpenIdAuthorizationUrlBuilder}; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; -credential_builder_impl!(OpenIdCredentialBuilder, OpenIdCredential); +credential_builder!(OpenIdCredentialBuilder, ConfidentialClientApplication); /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your @@ -18,6 +19,8 @@ credential_builder_impl!(OpenIdCredentialBuilder, OpenIdCredential); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc #[derive(Clone)] pub struct OpenIdCredential { + pub(crate) app_config: AppConfig, + /// Required unless requesting a refresh token /// The authorization code obtained from a call to authorize. /// The code should be obtained with all required scopes. @@ -26,10 +29,6 @@ pub struct OpenIdCredential { /// The refresh token needed to make an access token request using a refresh token. /// Do not include an authorization code when using a refresh token. pub(crate) refresh_token: Option<String>, - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, /// Required /// The application secret that you created in the app registration portal for your app. /// Don't use the application secret in a native app or single page app because a @@ -41,20 +40,17 @@ pub struct OpenIdCredential { /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, /// The same redirect_uri value that was used to acquire the authorization_code. - pub(crate) redirect_uri: Url, + // pub(crate) redirect_uri: Url, /// A space-separated list of scopes. The scopes must all be from a single resource, /// along with OIDC scopes (profile, openid, email). For more information, see Permissions /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension /// to the authorization code flow, intended to allow apps to declare the resource they want /// the token for during token redemption. pub(crate) scope: Vec<String>, - /// The Azure Active Directory tenant (directory) Id of the service principal. - pub(crate) authority: Authority, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } @@ -66,17 +62,21 @@ impl OpenIdCredential { redirect_uri: U, ) -> AuthorizationResult<OpenIdCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); - Ok(OpenIdCredential { + app_config: AppConfig { + tenant_id: None, + client_id: client_id.as_ref().to_owned(), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + }, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, - client_id: client_id.as_ref().to_owned(), client_secret: client_secret.as_ref().to_owned(), - redirect_uri: redirect_uri.into_url().or(redirect_uri_result)?, scope: vec!["openid".to_owned()], - authority: Default::default(), code_verifier: None, - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }) } @@ -96,15 +96,15 @@ impl OpenIdCredential { } #[async_trait] -impl TokenCredential for OpenIdCredential { +impl TokenCredentialExecutor for OpenIdCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.app_config.authority); if self.refresh_token.is_none() { let uri = self .serializer - .get(OAuthParameter::AccessTokenUrl) + .get(OAuthParameter::TokenUrl) .ok_or(AF::msg_internal_err("access_token_url"))?; Url::parse(uri.as_str()).map_err(AF::from) } else { @@ -117,7 +117,8 @@ impl TokenCredential for OpenIdCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.is_empty() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -126,7 +127,7 @@ impl TokenCredential for OpenIdCredential { } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .client_secret(self.client_secret.as_str()) .extend_scopes(self.scope.clone()); @@ -156,10 +157,13 @@ impl TokenCredential for OpenIdCredential { ); } + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { + self.serializer.redirect_uri(redirect_uri.as_str()); + } + self.serializer .authorization_code(authorization_code.as_ref()) - .grant_type("authorization_code") - .redirect_uri(self.redirect_uri.as_str()); + .grant_type("authorization_code"); if let Some(code_verifier) = self.code_verifier.as_ref() { self.serializer.code_verifier(code_verifier.as_str()); @@ -188,15 +192,22 @@ impl TokenCredential for OpenIdCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() } fn basic_auth(&self) -> Option<(String, String)> { - Some((self.client_id.clone(), self.client_secret.clone())) + Some(( + self.app_config.client_id.clone(), + self.client_secret.clone(), + )) + } + + fn app_config(&self) -> &AppConfig { + &self.app_config } } @@ -207,18 +218,17 @@ pub struct OpenIdCredentialBuilder { impl OpenIdCredentialBuilder { fn new() -> OpenIdCredentialBuilder { + let redirect_url = Url::parse("http://localhost").expect("Internal Error - please report"); + let mut app_config = AppConfig::new(); + app_config.redirect_uri = Some(redirect_url); Self { credential: OpenIdCredential { + app_config, authorization_code: None, refresh_token: None, - client_id: String::with_capacity(32), client_secret: String::new(), - redirect_uri: Url::parse("http://localhost") - .expect("Internal Error - please report"), scope: vec![], - authority: Default::default(), code_verifier: None, - token_credential_options: TokenCredentialOptions::default(), serializer: OAuthSerializer::new(), }, } @@ -238,7 +248,7 @@ impl OpenIdCredentialBuilder { /// Defaults to http://localhost pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { - self.credential.redirect_uri = redirect_uri.into_url()?; + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); Ok(self) } @@ -252,13 +262,17 @@ impl OpenIdCredentialBuilder { self } - pub fn with_proof_key_for_code_exchange( + pub fn with_pkce( &mut self, proof_key_for_code_exchange: &ProofKeyForCodeExchange, ) -> &mut Self { self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); self } + + pub fn credential(&self) -> &OpenIdCredential { + &self.credential + } } impl From<OpenIdAuthorizationUrl> for OpenIdCredentialBuilder { @@ -292,7 +306,7 @@ mod test { .with_authority(Authority::TenantId("common".into())) .build(); - assert_eq!(credential.authority, Authority::TenantId("common".into())) + assert_eq!(credential.authority(), Authority::TenantId("common".into())) } #[test] @@ -301,6 +315,6 @@ mod test { .with_authority(Authority::AzureDirectoryFederatedServices) .build(); - assert_eq!(credential.authority.as_ref(), "adfs"); + assert_eq!(credential.authority().as_ref(), "adfs"); } } diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 59e58880..d30ddfb4 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,6 +1,7 @@ +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredential, - TokenCredentialOptions, + Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, + TokenCredentialExecutor, TokenCredentialOptions, }; use async_trait::async_trait; use graph_error::AuthorizationResult; @@ -17,26 +18,34 @@ use url::Url; /// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 pub struct PublicClientApplication { http_client: reqwest::Client, - token_credential_options: TokenCredentialOptions, - credential: Box<dyn TokenCredential + Send>, + credential: Box<dyn TokenCredentialExecutor + Send>, } impl PublicClientApplication { - pub fn new<T>( - credential: T, - options: TokenCredentialOptions, - ) -> anyhow::Result<PublicClientApplication> + pub fn new<T>(credential: T) -> PublicClientApplication where T: Into<PublicClientApplication>, { - let mut public_client_application = credential.into(); - public_client_application.token_credential_options = options; - Ok(public_client_application) + credential.into() + } + + pub(crate) fn credential<T>(credential: T) -> PublicClientApplication + where + T: TokenCredentialExecutor + Send + 'static, + { + PublicClientApplication { + http_client: ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build() + .unwrap(), + credential: Box::new(credential), + } } } #[async_trait] -impl TokenCredential for PublicClientApplication { +impl TokenCredentialExecutor for PublicClientApplication { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.credential.uri(azure_authority_host) } @@ -49,13 +58,22 @@ impl TokenCredential for PublicClientApplication { self.credential.client_id() } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.credential.authority() + } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.credential.azure_cloud_instance() + } + + fn app_config(&self) -> &AppConfig { + self.credential.app_config() } - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host; + fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let azure_authority_host = self.azure_cloud_instance(); let uri = self.credential.uri(&azure_authority_host)?; + let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -80,9 +98,10 @@ impl TokenCredential for PublicClientApplication { } } - async fn get_token_async(&mut self) -> anyhow::Result<Response> { - let azure_authority_host = self.token_credential_options.azure_authority_host; - let uri = self.credential.uri(&azure_authority_host)?; + async fn execute_async(&mut self) -> anyhow::Result<Response> { + let azure_cloud_instance = self.credential.azure_cloud_instance(); + let uri = self.credential.uri(&azure_cloud_instance)?; + let form = self.credential.form_urlencode()?; let basic_auth = self.credential.basic_auth(); let mut headers = HeaderMap::new(); @@ -115,28 +134,12 @@ impl TokenCredential for PublicClientApplication { impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { fn from(value: ResourceOwnerPasswordCredential) -> Self { - PublicClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - } + PublicClientApplication::credential(value) } } impl From<DeviceCodeCredential> for PublicClientApplication { fn from(value: DeviceCodeCredential) -> Self { - PublicClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - token_credential_options: value.token_credential_options.clone(), - credential: Box::new(value), - } + PublicClientApplication::credential(value) } } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index a29ad4a4..81e1d402 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,5 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureCloudInstance, TokenCredential, TokenCredentialOptions}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, +}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; @@ -12,10 +15,7 @@ use url::Url; /// https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.3 #[derive(Clone)] pub struct ResourceOwnerPasswordCredential { - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, + pub(crate) app_config: AppConfig, /// Required /// The user's email address. pub(crate) username: String, @@ -27,8 +27,6 @@ pub struct ResourceOwnerPasswordCredential { /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, - pub(crate) authority: Authority, - pub(crate) token_credential_options: TokenCredentialOptions, serializer: OAuthSerializer, } @@ -38,30 +36,28 @@ impl ResourceOwnerPasswordCredential { username: T, password: T, ) -> ResourceOwnerPasswordCredential { + let mut app_config = AppConfig::new_with_client_id(client_id.as_ref()); + app_config.authority = Authority::Organizations; ResourceOwnerPasswordCredential { - client_id: client_id.as_ref().to_owned(), + app_config, username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), scope: vec![], - authority: Default::default(), - token_credential_options: Default::default(), serializer: Default::default(), } } pub fn new_with_tenant<T: AsRef<str>>( - tenant: T, + tenant_id: T, client_id: T, username: T, password: T, ) -> ResourceOwnerPasswordCredential { ResourceOwnerPasswordCredential { - client_id: client_id.as_ref().to_owned(), + app_config: AppConfig::init(tenant_id.as_ref().to_owned(), client_id), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), scope: vec![], - authority: Authority::TenantId(tenant.as_ref().to_owned()), - token_credential_options: Default::default(), serializer: Default::default(), } } @@ -72,20 +68,21 @@ impl ResourceOwnerPasswordCredential { } #[async_trait] -impl TokenCredential for ResourceOwnerPasswordCredential { +impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.app_config.authority); let uri = self .serializer - .get(OAuthParameter::AccessTokenUrl) + .get(OAuthParameter::TokenUrl) .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.trim().is_empty() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -98,7 +95,7 @@ impl TokenCredential for ResourceOwnerPasswordCredential { } self.serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .grant_type("password") .extend_scopes(self.scope.iter()); @@ -109,16 +106,20 @@ impl TokenCredential for ResourceOwnerPasswordCredential { } fn client_id(&self) -> &String { - &self.client_id + &self.app_config.client_id } - fn token_credential_options(&self) -> &TokenCredentialOptions { - &self.token_credential_options + fn authority(&self) -> Authority { + self.app_config.authority.clone() } fn basic_auth(&self) -> Option<(String, String)> { Some((self.username.to_string(), self.password.to_string())) } + + fn app_config(&self) -> &AppConfig { + todo!() + } } #[derive(Clone)] @@ -130,22 +131,23 @@ impl ResourceOwnerPasswordCredentialBuilder { fn new() -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { - client_id: String::with_capacity(32), + app_config: Default::default(), username: String::new(), password: String::new(), scope: vec![], - authority: Authority::Organizations, - token_credential_options: Default::default(), serializer: Default::default(), }, } } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - if self.credential.client_id.is_empty() { - self.credential.client_id.push_str(client_id.as_ref()); + if self.credential.app_config.client_id.is_empty() { + self.credential + .app_config + .client_id + .push_str(client_id.as_ref()); } else { - self.credential.client_id = client_id.as_ref().to_owned(); + self.credential.app_config.client_id = client_id.as_ref().to_owned(); } self } @@ -164,7 +166,7 @@ impl ResourceOwnerPasswordCredentialBuilder { /// Use /organizations or a tenant ID instead. /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.credential.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } @@ -199,7 +201,7 @@ impl ResourceOwnerPasswordCredentialBuilder { ); } - self.credential.authority = authority; + self.credential.app_config.authority = authority; Ok(self) } @@ -209,13 +211,6 @@ impl ResourceOwnerPasswordCredentialBuilder { self } - pub fn with_token_credential_options( - &mut self, - token_credential_options: TokenCredentialOptions, - ) { - self.credential.token_credential_options = token_credential_options; - } - pub fn build(&self) -> ResourceOwnerPasswordCredential { self.credential.clone() } diff --git a/graph-oauth/src/identity/credentials/token_credential.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs similarity index 58% rename from graph-oauth/src/identity/credentials/token_credential.rs rename to graph-oauth/src/identity/credentials/token_credential_executor.rs index 8126b2fc..c873d6bf 100644 --- a/graph-oauth/src/identity/credentials/token_credential.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -1,26 +1,60 @@ -use crate::identity::{AzureCloudInstance, TokenCredentialOptions}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialOptions}; +use crate::oauth::MsalTokenResponse; use async_trait::async_trait; use graph_error::AuthorizationResult; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; -use reqwest::ClientBuilder; +use reqwest::{ClientBuilder, ResponseBuilderExt}; use std::collections::HashMap; use url::Url; +/* +fn http_response(response: reqwest::blocking::Response) { + let status = response.status(); + let url = response.url().clone(); + let headers = response.headers().clone(); + let version = response.version(); + + let mut builder = http::Response::builder() + .url(url) + .status(http::StatusCode::from(&status)) + .version(version); + + for builder_header in builder.headers_mut().iter_mut() { + builder_header.extend(headers.clone()); + } + + let body_result: reqwest::Result<serde_json::Value> = response.json(); +// MsalTokenResponse + if let Ok(body) = body_result.as_ref() { + let token: serde_json::Result<MsalTokenResponse> = serde_json::from_value(body.clone()); + builder.json(body.clone()); + builder.body(token) + } else { + + } +} + + */ + #[async_trait] -pub trait TokenCredential { +pub trait TokenCredentialExecutor { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn client_id(&self) -> &String; - fn token_credential_options(&self) -> &TokenCredentialOptions; - + fn authority(&self) -> Authority; + fn azure_cloud_instance(&self) -> AzureCloudInstance { + AzureCloudInstance::AzurePublic + } fn basic_auth(&self) -> Option<(String, String)> { None } + fn app_config(&self) -> &AppConfig; - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let options = self.token_credential_options().clone(); - let uri = self.uri(&options.azure_authority_host)?; + fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let options = self.azure_cloud_instance(); + let uri = self.uri(&options)?; let form = self.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -45,9 +79,9 @@ pub trait TokenCredential { } } - async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { - let options = self.token_credential_options().clone(); - let uri = self.uri(&options.azure_authority_host)?; + async fn execute_async(&mut self) -> anyhow::Result<reqwest::Response> { + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.uri(&azure_cloud_instance)?; let form = self.form_urlencode()?; let http_client = ClientBuilder::new() .min_tls_version(Version::TLS_1_2) diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index aa3e222c..05aa616b 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,17 +1,19 @@ use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; use async_trait::async_trait; +use crate::identity::AzureCloudInstance; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; #[async_trait] pub trait TokenRequest: AuthorizationSerializer { - fn token_credential_options(&self) -> &TokenCredentialOptions; + fn azure_cloud_instance(&self) -> AzureCloudInstance; fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let options = self.token_credential_options().clone(); - let uri = self.uri(&options.azure_authority_host)?; + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.uri(&azure_cloud_instance)?; + let form = self.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -38,8 +40,9 @@ pub trait TokenRequest: AuthorizationSerializer { } async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { - let options = self.token_credential_options().clone(); - let uri = self.uri(&options.azure_authority_host)?; + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.uri(&azure_cloud_instance)?; + let form = self.form_urlencode()?; let http_client = ClientBuilder::new() .min_tls_version(Version::TLS_1_2) diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index dcc48fac..8f696771 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -60,7 +60,6 @@ extern crate serde; #[macro_use] extern crate log; extern crate pretty_env_logger; - mod access_token; mod auth; mod auth_response_query; diff --git a/src/lib.rs b/src/lib.rs index 3f4f6435..cef369cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,7 @@ //! .add_scope("offline_access") //! .redirect_uri("http://localhost:8000/redirect") //! .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") -//! .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") +//! .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") //! .response_type("code") //! .logout_url("https://login.microsoftonline.com/common/oauth2/v2.0/logout") diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs index faeb1e3e..926b8d04 100644 --- a/test-tools/src/oauth.rs +++ b/test-tools/src/oauth.rs @@ -9,7 +9,7 @@ impl OAuthTestTool { fn match_grant_credential(grant_request: GrantRequest) -> OAuthParameter { match grant_request { GrantRequest::Authorization => OAuthParameter::AuthorizationUrl, - GrantRequest::AccessToken => OAuthParameter::AccessTokenUrl, + GrantRequest::AccessToken => OAuthParameter::TokenUrl, GrantRequest::RefreshToken => OAuthParameter::RefreshTokenUrl, } } @@ -22,7 +22,7 @@ impl OAuthTestTool { ) { let mut url = String::new(); if grant_request.eq(&GrantRequest::AccessToken) { - let mut atu = oauth.get(OAuthParameter::AccessTokenUrl).unwrap(); + let mut atu = oauth.get(OAuthParameter::TokenUrl).unwrap(); if !atu.ends_with('?') { atu.push('?'); } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 977f51a4..093bf957 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,7 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - ClientSecretCredential, MsalTokenResponse, ResourceOwnerPasswordCredential, TokenCredential, + ClientSecretCredential, MsalTokenResponse, ResourceOwnerPasswordCredential, + TokenCredentialExecutor, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -121,12 +122,13 @@ impl OAuthTestCredentials { } fn client_credentials(self) -> ClientSecretCredential { - ClientSecretCredential::builder() + let mut credential = ClientSecretCredential::builder(); + credential .with_client_secret(self.client_secret.as_str()) .with_client_id(self.client_id.as_str()) .with_tenant(self.tenant.as_str()) - .with_scope(vec!["https://graph.microsoft.com/.default"]) - .build() + .with_scope(vec!["https://graph.microsoft.com/.default"]); + credential.credential() } fn resource_owner_password_credential(self) -> ResourceOwnerPasswordCredential { @@ -153,7 +155,7 @@ impl OAuthTestClient { match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); - if let Ok(response) = credential.get_token() { + if let Ok(response) = credential.execute() { let token: MsalTokenResponse = response.json().unwrap(); Some((user_id, token)) } else { @@ -162,7 +164,7 @@ impl OAuthTestClient { } OAuthTestClient::ResourceOwnerPasswordCredentials => { let mut credential = creds.resource_owner_password_credential(); - if let Ok(response) = credential.get_token() { + if let Ok(response) = credential.execute() { let token: MsalTokenResponse = response.json().unwrap(); Some((user_id, token)) } else { @@ -185,7 +187,7 @@ impl OAuthTestClient { match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); - match credential.get_token_async().await { + match credential.execute_async().await { Ok(response) => { let token: MsalTokenResponse = response.json().await.unwrap(); Some((user_id, token)) @@ -195,7 +197,7 @@ impl OAuthTestClient { } OAuthTestClient::ResourceOwnerPasswordCredentials => { let mut credential = creds.resource_owner_password_credential(); - match credential.get_token_async().await { + match credential.execute_async().await { Ok(response) => { let token: MsalTokenResponse = response.json().await.unwrap(); Some((user_id, token)) diff --git a/test_files/application_options/aad_options.json b/test_files/application_options/aad_options.json new file mode 100644 index 00000000..254c3313 --- /dev/null +++ b/test_files/application_options/aad_options.json @@ -0,0 +1,4 @@ +{ + "client_id": "1235", + "aad_authority_audience": "PersonalMicrosoftAccount" +} diff --git a/tests/discovery_tests.rs b/tests/discovery_tests.rs index 327700f7..7e74fedf 100644 --- a/tests/discovery_tests.rs +++ b/tests/discovery_tests.rs @@ -13,7 +13,7 @@ fn graph_discovery_oauth_v1() { Some(keys.authorization_endpoint.to_string()) ); assert_eq!( - oauth.get(OAuthParameter::AccessTokenUrl), + oauth.get(OAuthParameter::TokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( @@ -35,7 +35,7 @@ fn graph_discovery_oauth_v2() { Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthParameter::AccessTokenUrl), + oauth.get(OAuthParameter::TokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( @@ -57,7 +57,7 @@ async fn async_graph_discovery_oauth_v2() { Some(keys.authorization_endpoint) ); assert_eq!( - oauth.get(OAuthParameter::AccessTokenUrl), + oauth.get(OAuthParameter::TokenUrl), Some(keys.token_endpoint.to_string()) ); assert_eq!( diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index beeac811..8b5c9a7f 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -98,7 +98,7 @@ fn get_refresh_token() { .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .refresh_token_url("https://www.example.com/token?") .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); + .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write", "asfasf"); access_token.set_refresh_token("32LKLASDKJ"); @@ -119,7 +119,7 @@ fn multi_scope() { .add_scope("wl.offline_access") .redirect_uri("http://localhost:8000/redirect") .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .access_token_url("https://login.live.com/oauth20_token.srf") + .token_uri("https://login.live.com/oauth20_token.srf") .refresh_token_url("https://login.live.com/oauth20_token.srf") .response_type("code") .logout_url("https://login.live.com/oauth20_logout.srf?"); diff --git a/tests/oauth_tests.rs b/tests/oauth_tests.rs index a9313b84..1a64358e 100644 --- a/tests/oauth_tests.rs +++ b/tests/oauth_tests.rs @@ -10,7 +10,7 @@ fn oauth_parameters_from_credential() { .client_id("client_id") .client_secret("client_secret") .authorization_url("https://example.com/authorize?") - .access_token_url("https://example.com/token?") + .token_uri("https://example.com/token?") .refresh_token_url("https://example.com/token?") .redirect_uri("https://example.com/redirect?") .authorization_code("ADSLFJL4L3") @@ -43,7 +43,7 @@ fn oauth_parameters_from_credential() { oauth.get(credential), Some("https://example.com/authorize?".into()) ), - OAuthParameter::AccessTokenUrl => assert_eq!( + OAuthParameter::TokenUrl => assert_eq!( oauth.get(credential), Some("https://example.com/token?".into()) ), @@ -132,7 +132,7 @@ fn setters() { .client_secret("client_secret") .authorization_url("https://example.com/authorize") .refresh_token_url("https://example.com/token") - .access_token_url("https://example.com/token") + .token_uri("https://example.com/token") .redirect_uri("https://example.com/redirect") .authorization_code("access_code"); @@ -150,7 +150,7 @@ fn setters() { "https://example.com/authorize", ); test_setter(OAuthParameter::RefreshTokenUrl, "https://example.com/token"); - test_setter(OAuthParameter::AccessTokenUrl, "https://example.com/token"); + test_setter(OAuthParameter::TokenUrl, "https://example.com/token"); test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); test_setter(OAuthParameter::AuthorizationCode, "access_code"); } From 705d0a31a95a2619ef46e4baca6396af8eb02b68 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:41:47 -0400 Subject: [PATCH 030/118] Clippy and format --- examples/oauth/main.rs | 2 +- examples/oauth_certificate/main.rs | 2 +- .../credentials/application_builder.rs | 11 +++----- ...thorization_code_certificate_credential.rs | 4 +-- .../client_assertion_credential.rs | 2 +- .../credentials/client_builder_impl.rs | 1 - .../client_certificate_credential.rs | 8 +++--- .../credentials/client_secret_credential.rs | 2 +- .../confidential_client_application.rs | 27 +++++++------------ .../credentials/device_code_credential.rs | 4 +-- .../credentials/implicit_credential.rs | 4 +-- .../credentials/public_client_application.rs | 2 +- .../resource_owner_password_credential.rs | 6 ++--- .../credentials/token_credential_executor.rs | 6 ++--- .../src/identity/credentials/token_request.rs | 2 +- test-tools/src/oauth_request.rs | 2 +- tests/upload_request_blocking.rs | 2 +- 17 files changed, 34 insertions(+), 53 deletions(-) diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index be008331..117bfd03 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -72,7 +72,7 @@ async fn auth_code_grant(authorization_code: &str) { .with_pkce(&pkce) .build(); - let mut confidential_client = ConfidentialClientApplication::from(credential); + let mut confidential_client = credential; let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 54d4bec8..36434701 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -96,7 +96,7 @@ pub fn get_confidential_client( .with_redirect_uri("http://localhost:8080")? .build(); - Ok(ConfidentialClientApplication::from(credentials)) + Ok(credentials) } // When the authorization code comes in on the redirect from sign in, call the get_credential diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index fbe2a3a5..062ded11 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -4,12 +4,11 @@ use crate::identity::credentials::client_assertion_credential::ClientAssertionCr use crate::identity::X509Certificate; use crate::identity::{ application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, - AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredential, - AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCertificateCredential, - ClientCertificateCredentialBuilder, ClientCredentialsAuthorizationUrlBuilder, - ClientSecretCredentialBuilder, + AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, + AzureCloudInstance, ClientCertificateCredentialBuilder, + ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, }; -use crate::oauth::ConfidentialClientApplication; + use reqwest::header::HeaderMap; use std::collections::HashMap; use url::Url; @@ -362,8 +361,6 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { mod test { use super::*; use crate::oauth::AadAuthorityAudience; - use reqwest::header::AUTHORIZATION; - use wry::http::HeaderValue; #[test] #[should_panic] diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 2ea6c1ea..63dd400f 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -3,7 +3,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, - TokenCredentialOptions, CLIENT_ASSERTION_TYPE, + CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; @@ -75,7 +75,7 @@ impl AuthorizationCodeCertificateCredential { authority_url: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), - redirect_uri: redirect_uri.clone(), + redirect_uri, }; Ok(AuthorizationCodeCertificateCredential { diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 01b31d14..99fada9e 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationResult, AF}; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 284d6505..42a6b8d6 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -1,4 +1,3 @@ -use crate::identity::credentials::app_config::AppConfig; macro_rules! credential_builder_impl { ($name:ident, $credential:ty) => { impl $name { diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index a738202f..fda7b4a5 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,8 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{ - Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, -}; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use http_body_util::BodyExt; @@ -56,7 +54,7 @@ impl ClientCertificateCredential { ) -> anyhow::Result<ClientCertificateCredential> { let mut builder = ClientCertificateCredentialBuilder::new(); builder - .with_client_id(client_id.as_ref().to_owned()) + .with_client_id(client_id.as_ref()) .with_certificate(x509)?; Ok(builder.credential) } @@ -208,7 +206,7 @@ impl ClientCertificateCredentialBuilder { #[cfg(feature = "openssl")] pub fn with_certificate(&mut self, certificate: &X509Certificate) -> anyhow::Result<&mut Self> { - if let Some(tenant_id) = self.credential.authority.tenant_id() { + if let Some(tenant_id) = self.credential.app_config.authority.tenant_id() { self.with_client_assertion(certificate.sign_with_tenant(Some(tenant_id.clone()))?); } else { self.with_client_assertion(certificate.sign_with_tenant(None)?); diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 7734bcc9..e35e994e 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication, TokenCredentialExecutor, }; -use crate::oauth::TokenCredentialOptions; + use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; use std::collections::HashMap; diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index ee4fbc05..fcf38efb 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,24 +1,15 @@ -use crate::identity::application_options::ApplicationOptions; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::credentials::application_builder::{ - ClientCredentialParameter, ConfidentialClientApplicationBuilder, -}; +use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; use crate::identity::credentials::client_assertion_credential::ClientAssertionCredential; use crate::identity::{ - AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredential, - AuthorizationCodeCredential, AzureCloudInstance, ClientApplication, - ClientCertificateCredential, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredential, - CredentialStore, CredentialStoreType, InMemoryCredentialStore, OpenIdCredential, - TokenCacheProviderType, TokenCredentialExecutor, TokenCredentialOptions, -}; -use crate::oauth::{ - AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, - ClientCertificateCredentialBuilder, ClientSecretCredentialBuilder, - UnInitializedCredentialStore, + Authority, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, + AzureCloudInstance, ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, + TokenCredentialExecutor, }; + use async_trait::async_trait; -use graph_error::{AuthorizationResult, GraphResult}; -use http_body_util::BodyExt; +use graph_error::AuthorizationResult; + use reqwest::header::{HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; @@ -205,7 +196,7 @@ mod test { .unwrap() .build(); - let mut confidential_client = ConfidentialClientApplication::from(credential); + let mut confidential_client = credential; let credential_uri = confidential_client .credential .uri(&AzureCloudInstance::AzurePublic) @@ -226,7 +217,7 @@ mod test { .unwrap() .with_authority(Authority::Consumers) .build(); - let mut confidential_client = ConfidentialClientApplication::from(credential); + let mut confidential_client = credential; let credential_uri = confidential_client .credential .uri(&AzureCloudInstance::AzurePublic) diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 539f984a..62875bec 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,7 +1,5 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, -}; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use crate::oauth::{DeviceCode, PublicClientApplication}; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 1539c26c..63294758 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; -use crate::oauth::TokenCredentialOptions; +use crate::identity::{AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; + use graph_error::{AuthorizationFailure, AuthorizationResult}; use reqwest::IntoUrl; use url::form_urlencoded::Serializer; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index d30ddfb4..195e0fbb 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,7 +1,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, - TokenCredentialExecutor, TokenCredentialOptions, + TokenCredentialExecutor, }; use async_trait::async_trait; use graph_error::AuthorizationResult; diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 81e1d402..e0c18465 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,8 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{ - Authority, AzureCloudInstance, TokenCredentialExecutor, TokenCredentialOptions, -}; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::HashMap; @@ -54,7 +52,7 @@ impl ResourceOwnerPasswordCredential { password: T, ) -> ResourceOwnerPasswordCredential { ResourceOwnerPasswordCredential { - app_config: AppConfig::init(tenant_id.as_ref().to_owned(), client_id), + app_config: AppConfig::init(tenant_id.as_ref(), client_id), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), scope: vec![], diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index c873d6bf..561f4d06 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -1,11 +1,11 @@ use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialOptions}; -use crate::oauth::MsalTokenResponse; +use crate::identity::{Authority, AzureCloudInstance}; + use async_trait::async_trait; use graph_error::AuthorizationResult; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; -use reqwest::{ClientBuilder, ResponseBuilderExt}; +use reqwest::ClientBuilder; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 05aa616b..057c27a6 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,4 +1,4 @@ -use crate::oauth::{AuthorizationSerializer, TokenCredentialOptions}; +use crate::oauth::AuthorizationSerializer; use async_trait::async_trait; use crate::identity::AzureCloudInstance; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 093bf957..e9252da4 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -11,7 +11,7 @@ use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; -use std::path::Path; + use std::sync::Mutex; // static mutex's that are used for preventing test failures diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 5dfcad47..49eaaae1 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::*; +use std::thread; use std::time::Duration; -use std::{env, thread}; use test_tools::oauth_request::OAuthTestClient; fn get_special_folder_id(user_id: &str, folder: &str, client: &Graph) -> GraphResult<String> { From 4b44ba1d2f69f78dd25bd64c57b8ed40e2e162ad Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:47:57 -0400 Subject: [PATCH 031/118] Update tests --- .../credentials/implicit_credential.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 63294758..e7735896 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -349,7 +349,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("https://localhost/myapp") .unwrap() .with_scope(["User.Read"]) .with_response_mode(ResponseMode::Fragment) @@ -370,7 +370,7 @@ mod test { .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("https://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -389,7 +389,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_mode(ResponseMode::Fragment) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("https://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -408,7 +408,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -431,7 +431,7 @@ mod test { .into_iter() .collect(), )) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -450,7 +450,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::IdToken) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -469,7 +469,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") @@ -489,7 +489,7 @@ mod test { authorizer .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) - .with_redirect_uri("https::/localhost:8080/myapp") + .with_redirect_uri("https://example.com/myapp") .unwrap() .with_nonce("678910") .build_credential(); @@ -500,7 +500,7 @@ mod test { #[test] fn generate_nonce() { let url = ImplicitCredential::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("http://localhost:8080") .unwrap() .with_client_id("client_id") .with_scope(["read", "write"]) From af43cec417a8f725eecfc4f66f112c7b79248779 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 29 Aug 2023 03:38:29 -0400 Subject: [PATCH 032/118] Update examples and fix clippy lints --- .../auth_code_grant_pkce.rs | 0 .../auth_code_grant_refresh_token.rs | 0 .../auth_code_grant_secret.rs} | 0 examples/oauth/auth_code_grant/mod.rs | 3 + .../client_credentials_admin_consent.rs | 0 .../mod.rs} | 4 + examples/oauth/enable_pii_logging.rs | 12 - examples/oauth/main.rs | 5 - .../implicit_grant.rs | 2 +- examples/oauth_authorization_url/main.rs | 64 ++++ .../open_id_connect.rs | 12 + examples/oauth_certificate/main.rs | 47 ++- .../credentials/application_builder.rs | 295 +++++------------- .../client_assertion_credential.rs | 1 + .../credentials/client_builder_impl.rs | 44 --- .../client_certificate_credential.rs | 18 +- .../credentials/client_secret_credential.rs | 32 +- .../confidential_client_application.rs | 2 - .../credentials/implicit_credential.rs | 2 +- src/lib.rs | 62 ---- test-tools/src/oauth_request.rs | 16 +- 21 files changed, 198 insertions(+), 423 deletions(-) rename examples/oauth/{ => auth_code_grant}/auth_code_grant_pkce.rs (100%) rename examples/oauth/{ => auth_code_grant}/auth_code_grant_refresh_token.rs (100%) rename examples/oauth/{auth_code_grant.rs => auth_code_grant/auth_code_grant_secret.rs} (100%) create mode 100644 examples/oauth/auth_code_grant/mod.rs rename examples/oauth/{ => client_credentials}/client_credentials_admin_consent.rs (100%) rename examples/oauth/{client_credentials.rs => client_credentials/mod.rs} (96%) delete mode 100644 examples/oauth/enable_pii_logging.rs rename examples/{oauth => oauth_authorization_url}/implicit_grant.rs (97%) create mode 100644 examples/oauth_authorization_url/main.rs create mode 100644 examples/oauth_authorization_url/open_id_connect.rs diff --git a/examples/oauth/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs similarity index 100% rename from examples/oauth/auth_code_grant_pkce.rs rename to examples/oauth/auth_code_grant/auth_code_grant_pkce.rs diff --git a/examples/oauth/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs similarity index 100% rename from examples/oauth/auth_code_grant_refresh_token.rs rename to examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs diff --git a/examples/oauth/auth_code_grant.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs similarity index 100% rename from examples/oauth/auth_code_grant.rs rename to examples/oauth/auth_code_grant/auth_code_grant_secret.rs diff --git a/examples/oauth/auth_code_grant/mod.rs b/examples/oauth/auth_code_grant/mod.rs new file mode 100644 index 00000000..1a40cb11 --- /dev/null +++ b/examples/oauth/auth_code_grant/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_code_grant_pkce; +pub mod auth_code_grant_refresh_token; +pub mod auth_code_grant_secret; diff --git a/examples/oauth/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/client_credentials_admin_consent.rs similarity index 100% rename from examples/oauth/client_credentials_admin_consent.rs rename to examples/oauth/client_credentials/client_credentials_admin_consent.rs diff --git a/examples/oauth/client_credentials.rs b/examples/oauth/client_credentials/mod.rs similarity index 96% rename from examples/oauth/client_credentials.rs rename to examples/oauth/client_credentials/mod.rs index 2c432307..6bdd773c 100644 --- a/examples/oauth/client_credentials.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -14,6 +14,10 @@ use graph_rs_sdk::oauth::{ TokenCredentialExecutor, TokenRequest, }; +mod client_credentials_admin_consent; + +pub use client_credentials_admin_consent::*; + // This example shows programmatically getting an access token using the client credentials // flow after admin consent has been granted. If you have not granted admin consent, see // examples/client_credentials_admin_consent.rs for more info. diff --git a/examples/oauth/enable_pii_logging.rs b/examples/oauth/enable_pii_logging.rs deleted file mode 100644 index 124d9e37..00000000 --- a/examples/oauth/enable_pii_logging.rs +++ /dev/null @@ -1,12 +0,0 @@ -// By default the AccessToken access_token (bearer token) and id_token fields -// are logged or printed to the console as [REDACTED] by the AccessToken Debug implementation. - -// You can enable logging of these fields by setting the enable personally -// identifiable information field to true called enable_pii. - -use graph_rs_sdk::oauth::MsalTokenResponse; - -fn enable_pii_on_access_token(access_token: &mut MsalTokenResponse) { - access_token.enable_pii_logging(true); - println!("{access_token:#?}"); -} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 117bfd03..d6004530 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -16,14 +16,9 @@ extern crate serde; mod auth_code_grant; -mod auth_code_grant_pkce; -mod auth_code_grant_refresh_token; mod client_credentials; -mod client_credentials_admin_consent; mod device_code; -mod enable_pii_logging; mod environment_credential; -mod implicit_grant; mod is_access_token_expired; mod open_id_connect; mod signing_keys; diff --git a/examples/oauth/implicit_grant.rs b/examples/oauth_authorization_url/implicit_grant.rs similarity index 97% rename from examples/oauth/implicit_grant.rs rename to examples/oauth_authorization_url/implicit_grant.rs index a43c3b6b..a50bcb90 100644 --- a/examples/oauth/implicit_grant.rs +++ b/examples/oauth_authorization_url/implicit_grant.rs @@ -1,7 +1,7 @@ use std::collections::BTreeSet; // NOTICE: The Implicit Flow is considered legacy and cannot be used in a -// ConfidentialClientApplication or Public +// ConfidentialClientApplication or PublicClientApplication // The following example shows authenticating an application to use the OneDrive REST API // for a native client. Native clients typically use the implicit OAuth flow. This requires diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs new file mode 100644 index 00000000..c1fafa6c --- /dev/null +++ b/examples/oauth_authorization_url/main.rs @@ -0,0 +1,64 @@ +//! # Setup +//! +//! You will first need to setup an application in the azure portal. +//! +//! Microsoft Identity Platform: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-vs-authorization +#![allow(dead_code, unused, unused_imports)] + +#[macro_use] +extern crate serde; + +mod implicit_grant; +mod open_id_connect; + +use graph_rs_sdk::oauth::{ + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, + ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, + DeviceCodeCredential, MsalTokenResponse, ProofKeyForCodeExchange, PublicClientApplication, + TokenCredentialExecutor, TokenRequest, +}; + +fn main() {} + +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; + +// Authorization Code Grant Auth URL Builder +pub fn auth_code_grant_authorization() { + let url = AuthorizationCodeCredential::authorization_url_builder() + .with_client_id(CLIENT_ID) + .with_redirect_uri("http://localhost:8000/redirect") + .with_scope(vec!["offline_access", "files.read"]) + .url() + .unwrap(); + + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); +} + +// Authorization Code Grant PKCE + +// This example shows how to use a code_challenge and code_verifier +// to perform the authorization code grant flow with proof key for +// code exchange (PKCE). +// +// For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +// And the PKCE RFC: https://tools.ietf.org/html/rfc7636 + +// Open the default system web browser to the sign in url for authorization. +// This method uses AuthorizationCodeAuthorizationUrl to build the sign in +// url and query needed to get an authorization code and opens the default system +// web browser to this Url. +fn auth_code_grant_pkce_authorization() { + let pkce = ProofKeyForCodeExchange::generate().unwrap(); + + let url = AuthorizationCodeCredential::authorization_url_builder() + .with_client_id(CLIENT_ID) + .with_scope(vec!["user.read"]) + .with_redirect_uri("http://localhost:8000/redirect") + .with_pkce(&pkce) + .url() + .unwrap(); + + webbrowser::open(url.as_str()).unwrap(); +} diff --git a/examples/oauth_authorization_url/open_id_connect.rs b/examples/oauth_authorization_url/open_id_connect.rs new file mode 100644 index 00000000..4f584be4 --- /dev/null +++ b/examples/oauth_authorization_url/open_id_connect.rs @@ -0,0 +1,12 @@ +use graph_oauth::oauth::OpenIdCredential; +use url::Url; + +// Use your client id and client secret found in the Azure Portal +fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { + Ok(OpenIdCredential::authorization_url_builder()? + .with_client_id(client_id) + .with_default_scope()? + .extend_scope(vec!["Files.Read"]) + .build() + .url()?) +} diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 36434701..ca685b07 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -69,34 +69,21 @@ pub fn authorization_sign_in(client_id: &str, tenant_id: &str) { webbrowser::open(url.as_str()).unwrap(); } -pub fn get_confidential_client( - authorization_code: &str, - client_id: &str, - tenant_id: &str, -) -> anyhow::Result<ConfidentialClientApplication> { +pub fn x509_certificate(client_id: &str, tenant_id: &str) -> anyhow::Result<X509Certificate> { // Use include_bytes!(file_path) if the files are local - let mut cert_file = File::open(PRIVATE_KEY_PATH).unwrap(); + let mut cert_file = File::open(PRIVATE_KEY_PATH)?; let mut certificate: Vec<u8> = Vec::new(); cert_file.read_to_end(&mut certificate)?; - let mut private_key_file = File::open(CERTIFICATE_PATH).unwrap(); + let mut private_key_file = File::open(CERTIFICATE_PATH)?; let mut private_key: Vec<u8> = Vec::new(); private_key_file.read_to_end(&mut private_key)?; - let cert = X509::from_pem(certificate.as_slice()).unwrap(); - let pkey = PKey::private_key_from_pem(private_key.as_slice()).unwrap(); - - let x509_certificate = X509Certificate::new_with_tenant(client_id, tenant_id, cert, pkey); - - let credentials = AuthorizationCodeCertificateCredential::builder(authorization_code) - .with_client_id(client_id) - .with_tenant(tenant_id) - .with_x509(&x509_certificate)? - .with_scope(vec!["User.Read"]) - .with_redirect_uri("http://localhost:8080")? - .build(); - - Ok(credentials) + let cert = X509::from_pem(certificate.as_slice())?; + let pkey = PKey::private_key_from_pem(private_key.as_slice())?; + Ok(X509Certificate::new_with_tenant( + client_id, tenant_id, cert, pkey, + )) } // When the authorization code comes in on the redirect from sign in, call the get_credential @@ -111,18 +98,28 @@ async fn handle_redirect( // Print out the code for debugging purposes. println!("{:#?}", access_code.code); - let mut confidential_client = - get_confidential_client(access_code.code.as_str(), CLIENT_ID, TENANT).unwrap(); + let authorization_code = access_code.code; + let x509 = x509_certificate(CLIENT_ID, TENANT).unwrap(); + + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_authorization_code_certificate(authorization_code, &x509) + .unwrap() + .with_tenant(TENANT) + .with_scope(vec!["User.Read"]) + .with_redirect_uri("http://localhost:8080") + .unwrap() + .build(); // Returns reqwest::Response let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); if response.status().is_success() { - let access_token: MsalTokenResponse = response.json().await.unwrap(); + let mut msal_token: MsalTokenResponse = response.json().await.unwrap(); + msal_token.enable_pii_logging(true); // If all went well here we can print out the Access Token. - println!("AccessToken: {:#?}", access_token.access_token); + println!("AccessToken: {:#?}", msal_token); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 062ded11..6f8d60eb 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,102 +1,14 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::credentials::client_assertion_credential::ClientAssertionCredentialBuilder; -#[cfg(feature = "openssl")] -use crate::identity::X509Certificate; use crate::identity::{ application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, - AzureCloudInstance, ClientCertificateCredentialBuilder, - ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, + AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, }; - -use reqwest::header::HeaderMap; -use std::collections::HashMap; +#[cfg(feature = "openssl")] +use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; use url::Url; -macro_rules! application_builder_impl { - ($name:ident) => { - impl $name { - pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - self.client_id = client_id.as_ref().to_owned(); - self - } - - pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { - self.tenant_id = Some(tenant_id.as_ref().to_owned()); - self.authority = Authority::TenantId(tenant_id.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<AuthorityHost>, U: Into<Authority>>( - &mut self, - authority_host: T, - authority: U, - ) -> &mut Self { - self.authority_url = authority_host.into(); - self.authority = authority.into(); - self - } - - /// Adds a known Azure AD authority to the application to sign-in users specifying - /// the full authority Uri. See https://aka.ms/msal-net-application-configuration. - pub fn with_authority_uri(&mut self, authority_uri: Url) -> &mut Self { - self.authority_url = AuthorityHost::Uri(authority_uri); - self - } - - pub fn with_azure_cloud_instance( - &mut self, - azure_cloud_instance: AzureCloudInstance, - ) -> &mut Self { - self.authority_url = AuthorityHost::AzureCloudInstance(azure_cloud_instance); - self - } - - pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { - self.redirect_uri = Some(redirect_uri); - self.default_redirect_uri = false; - self - } - - pub fn with_default_redirect_uri(&mut self) -> &mut Self { - self.default_redirect_uri = true; - self - } - - pub fn with_extra_query_parameters<F: Fn(&mut HashMap<String, String>)>( - &mut self, - f: F, - ) -> &mut Self { - f(&mut self.extra_query_parameters); - self - } - - pub fn with_extra_header_parameters<F: Fn(&mut HeaderMap)>( - &mut self, - f: F, - ) -> &mut Self { - f(&mut self.extra_header_parameters); - self - } - } - }; -} - -/* -pub fn with_extra_query_parameters( - &mut self, - query_parameters: HashMap<String, String>, - ) -> &mut Self { - self.extra_query_parameters = query_parameters; - self - } - - pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { - self.extra_header_parameters = header_parameters; - self - } - */ - #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). @@ -123,29 +35,14 @@ impl Default for AuthorityHost { } } -pub enum ClientCredentialParameter { - #[cfg(feature = "openssl")] - Certificate(X509Certificate), - SecretString(String), - SignedAssertion(String), -} - pub struct ConfidentialClientApplicationBuilder { app_config: AppConfig, - default_redirect_uri: bool, - redirect_uri: Option<Url>, - client_credential_parameter: Option<ClientCredentialParameter>, } -// application_builder_impl!(ConfidentialClientApplicationBuilder); - impl ConfidentialClientApplicationBuilder { pub fn new(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { ConfidentialClientApplicationBuilder { app_config: AppConfig::new_with_client_id(client_id), - default_redirect_uri: false, - redirect_uri: None, - client_credential_parameter: None, } } @@ -202,7 +99,7 @@ impl ConfidentialClientApplicationBuilder { AuthorizationCodeCredentialBuilder::new_with_auth_code(self.into(), authorization_code) } - pub fn with_authorization_code_assertion_credential( + pub fn with_authorization_code_assertion( self, authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, @@ -215,7 +112,7 @@ impl ConfidentialClientApplicationBuilder { } #[cfg(feature = "openssl")] - pub fn with_authorization_code_certificate_credential( + pub fn with_authorization_code_certificate( self, authorization_code: impl AsRef<str>, x509: &X509Certificate, @@ -248,19 +145,6 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time" ); - /* - client_id: value.client_id, - tenant_id: value.tenant_id, - authority: value - .aad_authority_audience - .map(Authority::from) - .unwrap_or_default(), - authority_url: value - .azure_cloud_instance - .map(AuthorityHost::AzureCloudInstance) - .unwrap_or_default(), - */ - Ok(ConfidentialClientApplicationBuilder { app_config: AppConfig { tenant_id: value.tenant_id, @@ -277,46 +161,24 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { extra_header_parameters: Default::default(), redirect_uri: None, }, - default_redirect_uri: value.redirect_uri.is_none(), - redirect_uri: value.redirect_uri, - client_credential_parameter: None, }) } } -pub struct ConfidentialClientAppSelectionBuilder { - builder: ConfidentialClientApplicationBuilder, -} - -impl ConfidentialClientAppSelectionBuilder {} - +#[allow(dead_code)] pub struct PublicClientApplicationBuilder { - client_id: String, - tenant_id: Option<String>, - authority: Authority, - authority_url: AuthorityHost, - redirect_uri: Option<Url>, - default_redirect_uri: bool, - extra_query_parameters: HashMap<String, String>, - extra_header_parameters: HeaderMap, + app_config: AppConfig, } -application_builder_impl!(PublicClientApplicationBuilder); - impl PublicClientApplicationBuilder { + #[allow(dead_code)] pub fn new(client_id: &str) -> PublicClientApplicationBuilder { PublicClientApplicationBuilder { - client_id: client_id.to_owned(), - tenant_id: None, - authority: Default::default(), - authority_url: Default::default(), - default_redirect_uri: false, - redirect_uri: None, - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), + app_config: AppConfig::new_with_client_id(client_id), } } + #[allow(dead_code)] pub fn create_with_application_options( application_options: ApplicationOptions, ) -> anyhow::Result<PublicClientApplicationBuilder> { @@ -339,84 +201,85 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { ); Ok(PublicClientApplicationBuilder { - client_id: value.client_id, - tenant_id: value.tenant_id, - authority: value - .aad_authority_audience - .map(Authority::from) - .unwrap_or_default(), - authority_url: value - .azure_cloud_instance - .map(AuthorityHost::AzureCloudInstance) - .unwrap_or_default(), - default_redirect_uri: value.redirect_uri.is_none(), - redirect_uri: value.redirect_uri, - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), + app_config: AppConfig { + tenant_id: value.tenant_id, + client_id: value.client_id, + authority: value + .aad_authority_audience + .map(Authority::from) + .unwrap_or_default(), + authority_url: value + .azure_cloud_instance + .map(AuthorityHost::AzureCloudInstance) + .unwrap_or_default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + }, }) } } #[cfg(test)] mod test { - use super::*; - use crate::oauth::AadAuthorityAudience; - #[test] - #[should_panic] - fn confidential_client_error_result_on_instance_and_aci() { - ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), - tenant_id: None, - aad_authority_audience: None, - instance: Some(Url::parse("https://login.microsoft.com").unwrap()), - azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), - redirect_uri: None, - }) - .unwrap(); - } + /* + #[test] + #[should_panic] + fn confidential_client_error_result_on_instance_and_aci() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }) + .unwrap(); + } - #[test] - #[should_panic] - fn confidential_client_error_result_on_tenant_id_and_aad_audience() { - ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), - tenant_id: Some("tenant_id".to_owned()), - aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), - instance: None, - azure_cloud_instance: None, - redirect_uri: None, - }) - .unwrap(); - } + #[test] + #[should_panic] + fn confidential_client_error_result_on_tenant_id_and_aad_audience() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } - #[test] - #[should_panic] - fn public_client_error_result_on_instance_and_aci() { - PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), - tenant_id: None, - aad_authority_audience: None, - instance: Some(Url::parse("https://login.microsoft.com").unwrap()), - azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), - redirect_uri: None, - }) - .unwrap(); - } + #[test] + #[should_panic] + fn public_client_error_result_on_instance_and_aci() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }) + .unwrap(); + } - #[test] - #[should_panic] - fn public_client_error_result_on_tenant_id_and_aad_audience() { - PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), - tenant_id: Some("tenant_id".to_owned()), - aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), - instance: None, - azure_cloud_instance: None, - redirect_uri: None, - }) - .unwrap(); - } + #[test] + #[should_panic] + fn public_client_error_result_on_tenant_id_and_aad_audience() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } + */ /* #[test] diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 99fada9e..55b491d0 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -49,6 +49,7 @@ pub struct ClientAssertionCredentialBuilder { } impl ClientAssertionCredentialBuilder { + #[allow(dead_code)] pub(crate) fn new() -> ClientAssertionCredentialBuilder { ClientAssertionCredentialBuilder { credential: ClientAssertionCredential { diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 42a6b8d6..f220e8b7 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -1,47 +1,3 @@ -macro_rules! credential_builder_impl { - ($name:ident, $credential:ty) => { - impl $name { - pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - if self.credential.client_id.is_empty() { - self.credential.client_id.push_str(client_id.as_ref()); - } else { - self.credential.client_id = client_id.as_ref().to_owned(); - } - self - } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant(&mut self, tenant: impl AsRef<str>) -> &mut Self { - self.credential.authority = - crate::identity::Authority::TenantId(tenant.as_ref().to_owned()); - self - } - - pub fn with_authority<T: Into<crate::identity::Authority>>( - &mut self, - authority: T, - ) -> &mut Self { - self.credential.authority = authority.into(); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( - &mut self, - scope: I, - ) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - /* - pub fn build(&self) -> $credential { - self.credential.clone() - } - */ - } - }; -} - macro_rules! credential_builder_base { ($name:ident) => { impl $name { diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index fda7b4a5..083b19c7 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -3,7 +3,6 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; -use http_body_util::BodyExt; use std::collections::HashMap; use url::Url; @@ -178,21 +177,6 @@ impl ClientCertificateCredentialBuilder { } } - fn builder<T: ToString, I: IntoIterator<Item = T>>( - scopes: I, - ) -> ClientCertificateCredentialBuilder { - ClientCertificateCredentialBuilder { - credential: ClientCertificateCredential { - app_config: Default::default(), - scope: scopes.into_iter().map(|s| s.to_string()).collect(), - client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: String::new(), - refresh_token: None, - serializer: OAuthSerializer::new(), - }, - } - } - #[cfg(feature = "openssl")] pub(crate) fn new_with_certificate( x509: &X509Certificate, @@ -224,7 +208,7 @@ impl ClientCertificateCredentialBuilder { self } - pub(crate) fn credential(self) -> ClientCertificateCredential { + pub fn credential(self) -> ClientCertificateCredential { self.credential } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index e35e994e..31f29a10 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -66,14 +66,6 @@ impl ClientSecretCredential { } } - pub(crate) fn create() -> ClientSecretCredentialBuilder { - ClientSecretCredentialBuilder::new() - } - - pub fn builder() -> ClientSecretCredentialBuilder { - ClientSecretCredentialBuilder::new() - } - pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { ClientCredentialsAuthorizationUrlBuilder::new() } @@ -154,26 +146,6 @@ impl ClientSecretCredentialBuilder { } } - fn builder<T: ToString, I: IntoIterator<Item = T>>(scopes: I) -> ClientSecretCredentialBuilder { - let provided_scopes: Vec<String> = scopes.into_iter().map(|s| s.to_string()).collect(); - let scope = { - if provided_scopes.is_empty() { - vec!["https://graph.microsoft.com/.default".into()] - } else { - provided_scopes - } - }; - - Self { - credential: ClientSecretCredential { - app_config: Default::default(), - client_secret: String::new(), - scope, - serializer: Default::default(), - }, - } - } - pub(crate) fn new_with_client_secret( client_secret: impl AsRef<str>, app_config: AppConfig, @@ -194,8 +166,8 @@ impl ClientSecretCredentialBuilder { self } - pub fn credential(self) -> ClientSecretCredential { - self.credential + pub fn credential(&self) -> ClientSecretCredential { + self.credential.clone() } } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index fcf38efb..4156348e 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -17,8 +17,6 @@ use std::collections::HashMap; use url::Url; use wry::http::HeaderMap; -pub(crate) struct CredentialExecutor<T: TokenCredentialExecutor + Send>(T); - /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index e7735896..fbf950fa 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -144,7 +144,7 @@ impl ImplicitCredential { // id_token requires fragment or form_post. The Microsoft identity // platform recommends form_post. Unless you explicitly set // fragment then form_post is used here. Please file an issue - // if you experience encounter related problems. + // if you encounter related problems. if self.response_mode.eq(&ResponseMode::Query) { serializer.response_mode(ResponseMode::Fragment.as_ref()); } else { diff --git a/src/lib.rs b/src/lib.rs index cef369cf..4a0db384 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,68 +176,6 @@ //! - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) //! - [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) //! - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) -//! -//! # Example -//! ``` -//! use graph_rs_sdk::oauth::OAuthSerializer; -//! let mut oauth = OAuthSerializer::new(); -//! oauth -//! .client_id("<YOUR_CLIENT_ID>") -//! .client_secret("<YOUR_CLIENT_SECRET>") -//! .add_scope("files.read") -//! .add_scope("files.readwrite") -//! .add_scope("files.read.all") -//! .add_scope("files.readwrite.all") -//! .add_scope("offline_access") -//! .redirect_uri("http://localhost:8000/redirect") -//! .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") -//! .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token") -//! .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") -//! .response_type("code") -//! .logout_url("https://login.microsoftonline.com/common/oauth2/v2.0/logout") -//! .post_logout_redirect_uri("http://localhost:8000/redirect"); -//! ``` -//! Get the access code for the authorization code grant by sending the user to -//! log in using their browser. -//! ```rust,ignore -//! # use graph_rs_sdk::oauth::OAuth; -//! # let mut oauth = OAuth::new(); -//! let mut request = oauth.build().authorization_code_grant(); -//! let _ = request.browser_authorization().open(); -//! ``` -//! -//! The access code will be appended to the url on redirect. Pass -//! this code to the OAuth instance: -//! ```rust,ignore -//! # use graph_rs_sdk::oauth::OAuth; -//! # let mut oauth = OAuth::new(); -//! oauth.access_code("<ACCESS CODE>"); -//! ``` -//! -//! Perform an authorization code grant request for an access token: -//! ```rust,ignore -//! # use graph_rs_sdk::oauth::{AccessToken, OAuth}; -//! # let mut oauth = OAuth::new(); -//! let mut request = oauth.build().authorization_code_grant(); -//! -//! let response = request.access_token().send()?; -//! println!("{:#?}", access_token); -//! -//! if response.status().is_success() { -//! let mut access_token: AccessToken = response.json()?; -//! -//! let jwt = access_token.jwt(); -//! println!("{jwt:#?}"); -//! -//! // Store in OAuth to make requests for refresh tokens. -//! oauth.access_token(access_token); -//! } else { -//! // See if Microsoft Graph returned an error in the Response body -//! let result: reqwest::Result<serde_json::Value> = response.json(); -//! println!("{:#?}", result); -//! } -//! -//! ``` // mod client needs to stay on top of all other // client mod declarations for macro use. diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index e9252da4..e93f1154 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - ClientSecretCredential, MsalTokenResponse, ResourceOwnerPasswordCredential, - TokenCredentialExecutor, + ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, + ResourceOwnerPasswordCredential, TokenCredentialExecutor, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -122,13 +122,13 @@ impl OAuthTestCredentials { } fn client_credentials(self) -> ClientSecretCredential { - let mut credential = ClientSecretCredential::builder(); - credential - .with_client_secret(self.client_secret.as_str()) - .with_client_id(self.client_id.as_str()) + let mut builder = ConfidentialClientApplication::builder(self.client_id.as_str()) + .with_client_secret_credential(self.client_secret.as_str()); + + builder .with_tenant(self.tenant.as_str()) - .with_scope(vec!["https://graph.microsoft.com/.default"]); - credential.credential() + .with_scope(vec!["https://graph.microsoft.com/.default"]) + .credential() } fn resource_owner_password_credential(self) -> ResourceOwnerPasswordCredential { From fd88134acd94b4b33ddd0135504f03a826fe57d8 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 1 Sep 2023 03:58:52 -0400 Subject: [PATCH 033/118] Update OpenId and add onto builders and executor --- Cargo.toml | 4 +- examples/oauth/client_credentials/mod.rs | 2 +- examples/oauth/open_id_connect.rs | 104 ++---- examples/oauth_certificate/main.rs | 2 +- graph-error/Cargo.toml | 1 + graph-error/src/authorization_failure.rs | 8 + graph-http/src/core/file_config.rs | 2 +- graph-http/src/lib.rs | 2 - graph-http/src/pipeline/http_pipeline.rs | 67 ---- graph-http/src/pipeline/mod.rs | 3 - graph-oauth/src/access_token.rs | 2 +- graph-oauth/src/auth.rs | 8 +- graph-oauth/src/id_token.rs | 62 ++-- .../src/identity/application_options.rs | 13 + graph-oauth/src/identity/authority.rs | 6 + .../src/identity/credentials/app_config.rs | 9 +- .../credentials/application_builder.rs | 303 ++++++++++++------ ...thorization_code_certificate_credential.rs | 1 + .../authorization_code_credential.rs | 3 +- .../client_assertion_credential.rs | 7 +- .../credentials/client_builder_impl.rs | 50 +++ .../client_certificate_credential.rs | 1 + .../credentials/client_secret_credential.rs | 23 +- .../credentials/device_code_credential.rs | 2 +- .../credentials/implicit_credential.rs | 2 + .../credentials/open_id_authorization_url.rs | 9 +- .../credentials/open_id_credential.rs | 38 ++- .../proof_key_for_code_exchange.rs | 3 +- .../resource_owner_password_credential.rs | 4 +- .../credentials/token_credential_executor.rs | 86 +++++ test-tools/src/oauth_request.rs | 2 +- 31 files changed, 505 insertions(+), 324 deletions(-) delete mode 100644 graph-http/src/pipeline/http_pipeline.rs delete mode 100644 graph-http/src/pipeline/mod.rs diff --git a/Cargo.toml b/Cargo.toml index c6e19b0f..72289a17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ bytes = { version = "1.4.0" } futures = "0.3" lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } -warp = "0.3.3" +warp = { version = "0.3.5" } webbrowser = "0.8.7" anyhow = "1.0.69" log = "0.4" @@ -69,6 +69,8 @@ pretty_env_logger = "0.4" from_as = "0.2.0" actix = "0.13.0" actix-rt = "2.8.0" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index 6bdd773c..e0868cd7 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -42,7 +42,7 @@ pub async fn get_token_silent() { pub async fn get_token_silent2() { let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret_credential(CLIENT_SECRET) + .with_client_secret(CLIENT_SECRET) .build(); let response = confidential_client.execute_async().await.unwrap(); diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/open_id_connect.rs index e515036c..4b5fa2e9 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/open_id_connect.rs @@ -1,9 +1,11 @@ use graph_oauth::identity::{ - ConfidentialClientApplication, ResponseType, TokenCredentialExecutor, TokenRequest, + ConfidentialClientApplication, Prompt, ResponseMode, ResponseType, TokenCredentialExecutor, + TokenRequest, }; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{IdToken, MsalTokenResponse, OAuthSerializer}; use url::Url; + /// # Example /// ``` /// use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuth}; @@ -23,62 +25,48 @@ use warp::Filter; // The client id and client secret must be changed before running this example. static CLIENT_ID: &str = ""; static CLIENT_SECRET: &str = ""; - -fn open_id_credential( - authorization_code: &str, - client_id: &str, - client_secret: &str, -) -> anyhow::Result<ConfidentialClientApplication> { - Ok(OpenIdCredential::builder() - .with_authorization_code(authorization_code) - .with_client_id(client_id) - .with_client_secret(client_secret) - .with_redirect_uri("http://localhost:8000")? - .with_scope(vec!["offline_access", "Files.Read"]) // OpenIdCredential automatically sets the openid scope - .build()) -} +static TENANT_ID: &str = ""; fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { Ok(OpenIdCredential::authorization_url_builder()? .with_client_id(client_id) + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) .with_default_scope()? - .extend_scope(vec!["Files.Read"]) + .extend_scope(vec!["User.Read", "User.ReadWrite"]) .build() .url()?) } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct OpenIdResponse { - pub code: String, - pub id_token: String, - pub session_state: String, -} +async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + id_token.enable_pii_logging(true); + println!("{id_token:#?}"); -async fn handle_redirect( - id_token: OpenIdResponse, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - println!("Received IdToken: {id_token:#?}"); - let code = id_token.code.clone(); + let code = id_token.code.unwrap(); - let mut credential = open_id_credential(code.as_ref(), CLIENT_ID, CLIENT_SECRET).unwrap(); - let mut result = credential.execute_async().await; + let mut confidential_client = OpenIdCredential::builder() + .with_authorization_code(id_token.code) + .with_client_id(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) + .with_redirect_uri("http://localhost:8000/redirect")? + .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope + .build(); - dbg!(&result); + let mut response = confidential_client.execute_async().await.unwrap(); - if let Ok(response) = result { - if response.status().is_success() { - let mut access_token: MsalTokenResponse = response.json().await.unwrap(); - access_token.enable_pii_logging(true); + if response.status().is_success() { + let mut access_token: MsalTokenResponse = response.json().await.unwrap(); + access_token.enable_pii_logging(true); - println!("\n{:#?}\n", access_token); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - } + println!("\n{:#?}\n", access_token); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + println!("{result:#?}"); } - // Generic login page response. Ok(Box::new( "Successfully Logged In! You can close your browser.", )) @@ -94,40 +82,14 @@ async fn handle_redirect( /// } /// ``` pub async fn start_server_main() { - let routes = warp::get() + let routes = warp::post() .and(warp::path("redirect")) - .and(warp::body::json()) - .and_then(handle_redirect); - - std::env::set_var("RUST_LOG", "trace"); + .and(warp::body::form()) + .and_then(handle_redirect) + .with(warp::trace::named("executor")); let url = open_id_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); webbrowser::open(url.as_ref()); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } - -/* -fn oauth_open_id() -> OAuthSerializer { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id(CLIENT_ID) - .client_secret(CLIENT_SECRET) - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .redirect_uri("http://localhost:8000/redirect") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .response_type("id_token code") - .response_mode("form_post") - .add_scope("openid") - .add_scope("Files.Read") - .add_scope("Files.ReadWrite") - .add_scope("Files.Read.All") - .add_scope("Files.ReadWrite.All") - .add_scope("offline_access") - .nonce("7362CAEA-9CA5") - .prompt("login") - .state("12345"); - oauth -} - */ diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index ca685b07..67e3d132 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -102,7 +102,7 @@ async fn handle_redirect( let x509 = x509_certificate(CLIENT_ID, TENANT).unwrap(); let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_authorization_code_certificate(authorization_code, &x509) + .with_authorization_code_x509_certificate(authorization_code, &x509) .unwrap() .with_tenant(TENANT) .with_scope(vec!["User.Read"]) diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 19d9e666..5a2edceb 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["onedrive", "microsoft", "microsoft-graph", "api", "oauth"] categories = ["authentication", "web-programming::http-client"] [dependencies] +anyhow = { version = "1.0.69", features = ["backtrace"]} base64 = "0.21.0" futures = "0.3" handlebars = "2.0.2" diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 0bd8721e..29f40641 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -71,4 +71,12 @@ impl AuthorizationFailure { pub fn url_parse_error<T>(url_parse_error: url::ParseError) -> Result<T, AuthorizationFailure> { Err(AuthorizationFailure::UrlParseError(url_parse_error)) } + + pub fn condition(cond: bool, name: &str, msg: &str) -> AuthorizationResult<()> { + if cond { + AF::msg_result(name, msg) + } else { + Ok(()) + } + } } diff --git a/graph-http/src/core/file_config.rs b/graph-http/src/core/file_config.rs index e9b852f8..a28765a9 100644 --- a/graph-http/src/core/file_config.rs +++ b/graph-http/src/core/file_config.rs @@ -162,7 +162,7 @@ impl FileConfig { self } - /// Create all directories in the path given if they do not exist. + /// Create all directories in the path if they do not exist. /// /// # Example /// ```rust diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 9b2764f6..93c5ffd0 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -4,7 +4,6 @@ extern crate serde; mod blocking; mod client; mod core; -mod pipeline; mod request_components; mod request_handler; mod resource_identifier; @@ -23,7 +22,6 @@ pub(crate) mod internal { pub use crate::client::*; pub use crate::core::*; pub use crate::io_tools::*; - pub use crate::pipeline::*; pub use crate::request_components::*; pub use crate::request_handler::*; pub use crate::resource_identifier::*; diff --git a/graph-http/src/pipeline/http_pipeline.rs b/graph-http/src/pipeline/http_pipeline.rs deleted file mode 100644 index d3956c5e..00000000 --- a/graph-http/src/pipeline/http_pipeline.rs +++ /dev/null @@ -1,67 +0,0 @@ -use http::Request; -use serde_json::Value; -use std::error::Error; -use std::sync::Arc; - -pub struct RequestContext { - // ... request context fields -} -// request context impl somewhere - -// Just here as an example. The actual struct/impl would be different. -pub struct SomePolicyResult; - -pub trait HttpPipelinePolicy { - // Modify the request. - fn process_async( - &self, - context: &RequestContext, - request: &mut http::Request<Value>, - pipeline: &[Arc<dyn HttpPipelinePolicy>], - ) -> Result<SomePolicyResult, Box<dyn std::error::Error>>; - - fn process_next_async( - &self, - context: &RequestContext, - request: &mut http::Request<Value>, - pipeline: &[Arc<dyn HttpPipelinePolicy>], - ) -> Result<SomePolicyResult, Box<dyn std::error::Error>> { - pipeline[0].process_async(context, request, &pipeline[1..]) - } -} - -// Example only. Not exact at all. -pub struct ExponentialBackoffRetryPolicy { - // ... retry fields - pub min_retries: u32, -} - -impl HttpPipelinePolicy for ExponentialBackoffRetryPolicy { - fn process_async( - &self, - _context: &RequestContext, - _request: &mut Request<Value>, - _pipeline: &[Arc<dyn HttpPipelinePolicy>], - ) -> Result<SomePolicyResult, Box<dyn Error>> { - // modify request... - - Ok(SomePolicyResult) - } -} - -pub struct ThrottleRetryPolicy { - // ... impl -} - -impl HttpPipelinePolicy for ThrottleRetryPolicy { - fn process_async( - &self, - _context: &RequestContext, - _request: &mut Request<Value>, - _pipeline: &[Arc<dyn HttpPipelinePolicy>], - ) -> Result<SomePolicyResult, Box<dyn Error>> { - // modify request... - - Ok(SomePolicyResult) - } -} diff --git a/graph-http/src/pipeline/mod.rs b/graph-http/src/pipeline/mod.rs deleted file mode 100644 index 647ef572..00000000 --- a/graph-http/src/pipeline/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod http_pipeline; - -pub use http_pipeline::*; diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index 381b4658..ea7cec05 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -218,7 +218,7 @@ impl MsalTokenResponse { self.id_token = Some(id_token.get_id_token()); } - pub fn parse_id_token(&mut self) -> Option<Result<IdToken, serde_json::Error>> { + pub fn parse_id_token(&mut self) -> Option<Result<IdToken, serde::de::value::Error>> { self.id_token.clone().map(|s| IdToken::from_str(s.as_str())) } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 68a78c6a..0c205e98 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -504,16 +504,16 @@ impl OAuthSerializer { /// oauth.id_token(IdToken::new("1345", "code", "state", "session_state")); /// ``` pub fn id_token(&mut self, value: IdToken) -> &mut OAuthSerializer { - if let Some(code) = value.get_code() { + if let Some(code) = value.code { self.authorization_code(code.as_str()); } - if let Some(state) = value.get_state() { + if let Some(state) = value.state { let _ = self.entry_with(OAuthParameter::State, state.as_str()); } - if let Some(session_state) = value.get_session_state() { + if let Some(session_state) = value.session_state { self.session_state(session_state.as_str()); } - self.insert(OAuthParameter::IdToken, value.get_id_token().as_str()) + self.insert(OAuthParameter::IdToken, value.id_token.as_str()) } /// Set the session state. diff --git a/graph-oauth/src/id_token.rs b/graph-oauth/src/id_token.rs index 2232df68..6db4c300 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-oauth/src/id_token.rs @@ -1,23 +1,22 @@ use crate::jwt::{JsonWebToken, JwtParser}; -use serde::de::{Error, Visitor}; +use serde::de::{Error, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; -use std::borrow::Cow; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{Debug, Formatter}; use std::str::FromStr; -use url::form_urlencoded; +use url::form_urlencoded::parse; #[derive(Default, Clone, Eq, PartialEq, Serialize)] pub struct IdToken { - code: Option<String>, - id_token: String, - state: Option<String>, - session_state: Option<String>, + pub code: Option<String>, + pub id_token: String, + pub state: Option<String>, + pub session_state: Option<String>, #[serde(flatten)] - additional_fields: HashMap<String, Value>, + pub additional_fields: HashMap<String, Value>, #[serde(skip)] log_pii: bool, } @@ -80,7 +79,7 @@ impl IdToken { } impl TryFrom<String> for IdToken { - type Error = std::io::Error; + type Error = serde::de::value::Error; fn try_from(value: String) -> Result<Self, Self::Error> { let id_token: IdToken = IdToken::from_str(value.as_str())?; @@ -89,7 +88,7 @@ impl TryFrom<String> for IdToken { } impl TryFrom<&str> for IdToken { - type Error = std::io::Error; + type Error = serde::de::value::Error; fn try_from(value: &str) -> Result<Self, Self::Error> { let id_token: IdToken = IdToken::from_str(value)?; @@ -135,22 +134,26 @@ impl<'de> Deserialize<'de> for IdToken { where E: Error, { - IdToken::from_str(v).map_err(|err| Error::custom(err)) + let d = serde_urlencoded::Deserializer::new(parse(v.as_bytes())); + d.deserialize_str(IdTokenVisitor) + .map_err(|err| Error::custom(err)) } fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> where E: serde::de::Error, { - let vec: Vec<(Cow<str>, Cow<str>)> = form_urlencoded::parse(v).collect(); - - if vec.is_empty() { - return serde_json::from_slice(v) - .map_err(|err| serde::de::Error::custom(err.to_string())); - } + let d = serde_urlencoded::Deserializer::new(parse(v)); + d.deserialize_bytes(IdTokenVisitor) + .map_err(|err| Error::custom(err)) + } + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'de>, + { let mut id_token = IdToken::default(); - for (key, value) in vec.iter() { + while let Ok(Some((key, value))) = map.next_entry::<String, String>() { match key.as_bytes() { b"code" => id_token.code(value.as_ref()), b"id_token" => id_token.id_token(value.as_ref()), @@ -163,6 +166,7 @@ impl<'de> Deserialize<'de> for IdToken { } } } + Ok(id_token) } } @@ -171,27 +175,9 @@ impl<'de> Deserialize<'de> for IdToken { } impl FromStr for IdToken { - type Err = serde_json::Error; + type Err = serde::de::value::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - let vec: Vec<(Cow<str>, Cow<str>)> = form_urlencoded::parse(s.as_bytes()).collect(); - if vec.is_empty() { - return serde_json::from_slice(s.as_bytes()); - } - let mut id_token = IdToken::default(); - for (key, value) in vec.iter() { - match key.as_bytes() { - b"code" => id_token.code(value.as_ref()), - b"id_token" => id_token.id_token(value.as_ref()), - b"state" => id_token.state(value.as_ref()), - b"session_state" => id_token.session_state(value.as_ref()), - _ => { - id_token - .additional_fields - .insert(key.to_string(), Value::String(value.to_string())); - } - } - } - Ok(id_token) + serde_urlencoded::from_str(s) } } diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 8d35b044..dd2487a1 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -34,6 +34,19 @@ pub struct ApplicationOptions { pub redirect_uri: Option<Url>, } +impl ApplicationOptions { + pub fn new(client_id: impl AsRef<str>) -> ApplicationOptions { + ApplicationOptions { + client_id: client_id.as_ref().to_owned(), + tenant_id: None, + aad_authority_audience: None, + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 3fc1be5b..6d019ae0 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -19,6 +19,12 @@ pub enum AzureCloudInstance { AzureUsGovernment, } +impl AzureCloudInstance { + pub fn get_open_id_configuration_url(&self, authority: Authority) -> String { + format!("{}/v2.0/{}", self.as_ref(), authority.as_ref()) + } +} + impl AsRef<str> for AzureCloudInstance { fn as_ref(&self) -> &str { match self { diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index dd1812db..86c8a244 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -6,6 +6,10 @@ use url::Url; #[derive(Clone, Debug, Default, PartialEq)] pub struct AppConfig { + /// The directory tenant that you want to request permission from. + /// This can be in GUID or friendly name format. + /// If you don't know which tenant the user belongs to + /// and you want to let them sign in with any tenant, use common. pub(crate) tenant_id: Option<String>, /// Required. /// The Application (client) ID that the Azure portal - App registrations page assigned @@ -47,7 +51,10 @@ impl AppConfig { } } - pub(crate) fn init(tenant_id: impl AsRef<str>, client_id: impl AsRef<str>) -> AppConfig { + pub(crate) fn new_with_tenant_and_client_id( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + ) -> AppConfig { AppConfig { tenant_id: Some(tenant_id.as_ref().to_string()), client_id: client_id.as_ref().to_string(), diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 6f8d60eb..7ca3d17e 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -7,6 +7,10 @@ use crate::identity::{ }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; +use crate::oauth::OpenIdCredentialBuilder; +use graph_error::{AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; +use std::collections::HashMap; use url::Url; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -36,11 +40,11 @@ impl Default for AuthorityHost { } pub struct ConfidentialClientApplicationBuilder { - app_config: AppConfig, + pub(crate) app_config: AppConfig, } impl ConfidentialClientApplicationBuilder { - pub fn new(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { + pub fn new(client_id: impl AsRef<str>) -> Self { ConfidentialClientApplicationBuilder { app_config: AppConfig::new_with_client_id(client_id), } @@ -48,17 +52,64 @@ impl ConfidentialClientApplicationBuilder { pub fn new_with_application_options( application_options: ApplicationOptions, - ) -> anyhow::Result<ConfidentialClientApplicationBuilder> { - ConfidentialClientApplicationBuilder::try_from(application_options) + ) -> AuthorizationResult<ConfidentialClientApplicationBuilder> { + Ok(ConfidentialClientApplicationBuilder::try_from( + application_options, + )?) + } + + pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + let tenant = tenant_id.as_ref().to_string(); + self.app_config.tenant_id = Some(tenant.clone()); + self.app_config.authority = Authority::TenantId(tenant); + self } - pub fn get_authorization_request_url<T: ToString, I: IntoIterator<Item = T>>( + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { + self.app_config + .extra_query_parameters + .insert(query_param.0, query_param.1); + self + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_parameters( &mut self, - scopes: I, - ) -> AuthCodeAuthorizationUrlParameterBuilder { - let mut builder = AuthCodeAuthorizationUrlParameterBuilder::new(); - builder.with_scope(scopes); - builder + query_parameters: HashMap<String, String>, + ) -> &mut Self { + self.app_config + .extra_query_parameters + .extend(query_parameters); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_param<K: Into<HeaderName>, V: Into<HeaderValue>>( + &mut self, + header_name: K, + header_value: V, + ) -> &mut Self { + self.app_config + .extra_header_parameters + .insert(header_name.into(), header_value.into()); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { + self.app_config + .extra_header_parameters + .extend(header_parameters); + self + } + + pub fn get_authorization_request_url(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new() } pub fn get_client_credential_request_url( @@ -68,21 +119,21 @@ impl ConfidentialClientApplicationBuilder { } #[cfg(feature = "openssl")] - pub fn with_client_certificate_credential( + pub fn with_client_x509_certificate( self, certificate: &X509Certificate, ) -> anyhow::Result<ClientCertificateCredentialBuilder> { ClientCertificateCredentialBuilder::new_with_certificate(certificate, self.app_config) } - pub fn with_client_secret_credential( + pub fn with_client_secret( self, client_secret: impl AsRef<str>, ) -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) } - pub fn with_client_assertion_credential( + pub fn with_client_assertion( self, signed_assertion: impl AsRef<str>, ) -> ClientAssertionCredentialBuilder { @@ -92,7 +143,7 @@ impl ConfidentialClientApplicationBuilder { ) } - pub fn with_authorization_code_credential( + pub fn with_authorization_code( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { @@ -112,7 +163,7 @@ impl ConfidentialClientApplicationBuilder { } #[cfg(feature = "openssl")] - pub fn with_authorization_code_certificate( + pub fn with_authorization_code_x509_certificate( self, authorization_code: impl AsRef<str>, x509: &X509Certificate, @@ -123,6 +174,18 @@ impl ConfidentialClientApplicationBuilder { x509, ) } + + pub fn with_open_id( + self, + authorization_code: impl AsRef<str>, + client_secret: impl AsRef<str>, + ) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new_with_auth_code_and_secret( + authorization_code, + client_secret, + self.app_config, + ) + } } impl From<ConfidentialClientApplicationBuilder> for AppConfig { @@ -132,18 +195,24 @@ impl From<ConfidentialClientApplicationBuilder> for AppConfig { } impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { - type Error = anyhow::Error; + type Error = AF; fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { - anyhow::ensure!(!value.client_id.is_empty(), "Client id cannot be empty"); - anyhow::ensure!( + AF::condition( + !value.client_id.is_empty(), + "Client Id", + "Client Id cannot be empty", + )?; + AF::condition( !(value.instance.is_some() && value.azure_cloud_instance.is_some()), - "Instance and AzureCloudInstance both specify the azure cloud instance and cannot be set at the same time" - ); - anyhow::ensure!( + "Instance | AzureCloudInstance", + "Both specify the azure cloud instance and cannot be set at the same time", + )?; + AF::condition( !(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), - "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time" - ); + "TenantId | AadAuthorityAudience", + "Both represent an authority audience and cannot be set at the same time", + )?; Ok(ConfidentialClientApplicationBuilder { app_config: AppConfig { @@ -181,24 +250,32 @@ impl PublicClientApplicationBuilder { #[allow(dead_code)] pub fn create_with_application_options( application_options: ApplicationOptions, - ) -> anyhow::Result<PublicClientApplicationBuilder> { - PublicClientApplicationBuilder::try_from(application_options) + ) -> AuthorizationResult<PublicClientApplicationBuilder> { + Ok(PublicClientApplicationBuilder::try_from( + application_options, + )?) } } impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { - type Error = anyhow::Error; + type Error = AF; fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { - anyhow::ensure!(!value.client_id.is_empty(), "Client id cannot be empty"); - anyhow::ensure!( + AF::condition( + !value.client_id.is_empty(), + "client_id", + "Client id cannot be empty", + )?; + AF::condition( !(value.instance.is_some() && value.azure_cloud_instance.is_some()), - "Instance and AzureCloudInstance both specify the azure cloud instance and cannot be set at the same time" - ); - anyhow::ensure!( + "Instance | AzureCloudInstance", + "Instance and AzureCloudInstance both specify the azure cloud instance and cannot be set at the same time", + )?; + AF::condition( !(value.tenant_id.is_some() && value.aad_authority_audience.is_some()), - "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time" - ); + "TenantId | AadAuthorityAudience", + "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time", + )?; Ok(PublicClientApplicationBuilder { app_config: AppConfig { @@ -222,77 +299,93 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { #[cfg(test)] mod test { + use super::*; + use crate::identity::AadAuthorityAudience; + use http::header::AUTHORIZATION; + use http::HeaderValue; + + #[test] + #[should_panic] + fn confidential_client_error_result_on_instance_and_aci() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }) + .unwrap(); + } + + #[test] + #[should_panic] + fn confidential_client_error_result_on_tenant_id_and_aad_audience() { + ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } + + #[test] + #[should_panic] + fn public_client_error_result_on_instance_and_aci() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_string(), + tenant_id: None, + aad_authority_audience: None, + instance: Some(Url::parse("https://login.microsoft.com").unwrap()), + azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), + redirect_uri: None, + }) + .unwrap(); + } + + #[test] + #[should_panic] + fn public_client_error_result_on_tenant_id_and_aad_audience() { + PublicClientApplicationBuilder::try_from(ApplicationOptions { + client_id: "client-id".to_owned(), + tenant_id: Some("tenant_id".to_owned()), + aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), + instance: None, + azure_cloud_instance: None, + redirect_uri: None, + }) + .unwrap(); + } - /* - #[test] - #[should_panic] - fn confidential_client_error_result_on_instance_and_aci() { - ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), - tenant_id: None, - aad_authority_audience: None, - instance: Some(Url::parse("https://login.microsoft.com").unwrap()), - azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), - redirect_uri: None, - }) - .unwrap(); - } - - #[test] - #[should_panic] - fn confidential_client_error_result_on_tenant_id_and_aad_audience() { - ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), - tenant_id: Some("tenant_id".to_owned()), - aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), - instance: None, - azure_cloud_instance: None, - redirect_uri: None, - }) - .unwrap(); - } - - #[test] - #[should_panic] - fn public_client_error_result_on_instance_and_aci() { - PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), - tenant_id: None, - aad_authority_audience: None, - instance: Some(Url::parse("https://login.microsoft.com").unwrap()), - azure_cloud_instance: Some(AzureCloudInstance::AzurePublic), - redirect_uri: None, - }) - .unwrap(); - } - - #[test] - #[should_panic] - fn public_client_error_result_on_tenant_id_and_aad_audience() { - PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), - tenant_id: Some("tenant_id".to_owned()), - aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), - instance: None, - azure_cloud_instance: None, - redirect_uri: None, - }) - .unwrap(); - } - */ - - /* - #[test] - fn extra_parameters() { - let mut confidential_client = ConfidentialClientApplicationBuilder::new("client-id"); - confidential_client.with_extra_query_parameters(|query| { - query.insert("name".into(), "123".into()); - }) - .with_extra_header_parameters(|map| { - map.insert(AUTHORIZATION, HeaderValue::from_static("Bearer Token")); - }); - assert_eq!(confidential_client.extra_header_parameters.get(AUTHORIZATION).unwrap(), &HeaderValue::from_static("Bearer Token")); - assert_eq!(confidential_client.extra_query_parameters.get("name").unwrap(), &String::from("123")); - } - */ + #[test] + fn extra_parameters() { + let mut confidential_client = ConfidentialClientApplicationBuilder::new("client-id"); + let mut map = HashMap::new(); + map.insert("name".to_owned(), "123".to_owned()); + confidential_client.with_extra_query_parameters(map); + + let mut header_map = HeaderMap::new(); + header_map.insert(AUTHORIZATION, HeaderValue::from_static("Bearer Token")); + confidential_client.with_extra_header_parameters(header_map); + + assert_eq!( + confidential_client + .app_config + .extra_header_parameters + .get(AUTHORIZATION) + .unwrap(), + &HeaderValue::from_static("Bearer Token") + ); + assert_eq!( + confidential_client + .app_config + .extra_query_parameters + .get("name") + .unwrap(), + &String::from("123") + ); + } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 63dd400f..1c7ef007 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -7,6 +7,7 @@ use crate::identity::{ }; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 1d5dd50c..b8e0241a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -7,6 +7,7 @@ use crate::identity::{ use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; @@ -66,7 +67,7 @@ impl AuthorizationCodeCredential { authorization_code: T, ) -> AuthorizationResult<AuthorizationCodeCredential> { Ok(AuthorizationCodeCredential { - app_config: AppConfig::init(tenant_id, client_id), + app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 55b491d0..3272b197 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -5,6 +5,7 @@ use crate::identity::{ use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; @@ -15,7 +16,6 @@ credential_builder!( #[derive(Clone)] pub struct ClientAssertionCredential { - /// The client (application) ID of the service principal pub(crate) app_config: AppConfig, /// The value passed for the scope parameter in this request should be the resource /// identifier (application ID URI) of the resource you want, affixed with the .default @@ -30,14 +30,15 @@ pub struct ClientAssertionCredential { impl ClientAssertionCredential { pub fn new( + assertion: impl AsRef<str>, tenant_id: impl AsRef<str>, client_id: impl AsRef<str>, ) -> ClientAssertionCredential { ClientAssertionCredential { - app_config: AppConfig::init(tenant_id, client_id), + app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: Default::default(), + client_assertion: assertion.as_ref().to_string(), refresh_token: None, serializer: Default::default(), } diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index f220e8b7..f34473fe 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -36,6 +36,56 @@ macro_rules! credential_builder_base { self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { + self.credential + .app_config + .extra_query_parameters + .insert(query_param.0, query_param.1); + self + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_parameters( + &mut self, + query_parameters: HashMap<String, String>, + ) -> &mut Self { + self.credential + .app_config + .extra_query_parameters + .extend(query_parameters); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_param<K: Into<HeaderName>, V: Into<HeaderValue>>( + &mut self, + header_name: K, + header_value: V, + ) -> &mut Self { + self.credential + .app_config + .extra_header_parameters + .insert(header_name.into(), header_value.into()); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_parameters( + &mut self, + header_parameters: HeaderMap, + ) -> &mut Self { + self.credential + .app_config + .extra_header_parameters + .extend(header_parameters); + self + } } }; } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 083b19c7..08ebab26 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -3,6 +3,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 31f29a10..c30f25ce 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -7,6 +7,7 @@ use crate::identity::{ use async_trait::async_trait; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; @@ -59,7 +60,7 @@ impl ClientSecretCredential { client_secret: T, ) -> ClientSecretCredential { ClientSecretCredential { - app_config: AppConfig::init(tenant_id, client_id), + app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), @@ -81,7 +82,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { self.serializer .get(OAuthParameter::TokenUrl) .ok_or(AuthorizationFailure::msg_err( - "access_token_url", + "token_url for access and refresh tokens missing", "Internal Error", ))?; Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) @@ -135,14 +136,9 @@ pub struct ClientSecretCredentialBuilder { } impl ClientSecretCredentialBuilder { - fn new() -> Self { - Self { - credential: ClientSecretCredential { - app_config: Default::default(), - client_secret: String::new(), - scope: vec!["https://graph.microsoft.com/.default".into()], - serializer: Default::default(), - }, + pub fn new<T: AsRef<str>>(client_id: T, client_secret: T) -> Self { + ClientSecretCredentialBuilder { + credential: ClientSecretCredential::new(client_id, client_secret), } } @@ -150,7 +146,6 @@ impl ClientSecretCredentialBuilder { client_secret: impl AsRef<str>, app_config: AppConfig, ) -> ClientSecretCredentialBuilder { - println!("{:#?}", &app_config); Self { credential: ClientSecretCredential { app_config, @@ -170,9 +165,3 @@ impl ClientSecretCredentialBuilder { self.credential.clone() } } - -impl Default for ClientSecretCredentialBuilder { - fn default() -> Self { - Self::new() - } -} diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 62875bec..4c745f0d 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use crate::oauth::{DeviceCode, PublicClientApplication}; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; - +use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use crate::identity::credentials::app_config::AppConfig; diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index fbf950fa..737876ce 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -3,7 +3,9 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; +use std::collections::HashMap; use url::form_urlencoded::Serializer; use url::Url; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 6db92d90..e1ed19f6 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -229,6 +229,10 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { serializer.response_types(self.response_type.iter()); } + if let Some(response_mode) = self.response_mode.as_ref() { + serializer.response_mode(response_mode.as_ref()); + } + if let Some(redirect_uri) = self.redirect_uri.as_ref() { serializer.redirect_uri(redirect_uri.as_ref()); } @@ -253,13 +257,13 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { serializer.encode_query( vec![ OAuthParameter::RedirectUri, - OAuthParameter::ResponseMode, OAuthParameter::State, OAuthParameter::Prompt, OAuthParameter::LoginHint, OAuthParameter::DomainHint, ], vec![ + OAuthParameter::ResponseMode, OAuthParameter::ClientId, OAuthParameter::ResponseType, OAuthParameter::Scope, @@ -370,9 +374,12 @@ impl OpenIdAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { + /* if !response_mode.eq(&ResponseMode::Query) { self.auth_url_parameters.response_mode = Some(response_mode); } + */ + self.auth_url_parameters.response_mode = Some(response_mode); self } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index e26e2159..0f3bc812 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -7,6 +7,7 @@ use crate::identity::{ use crate::oauth::{ConfidentialClientApplication, OpenIdAuthorizationUrlBuilder}; use async_trait::async_trait; use graph_error::{AuthorizationResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; @@ -51,6 +52,9 @@ pub struct OpenIdCredential { /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, + /// Used only when the client generates the pkce itself when the generate method + /// is called. + pub(crate) pkce: Option<ProofKeyForCodeExchange>, serializer: OAuthSerializer, } @@ -77,6 +81,7 @@ impl OpenIdCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["openid".to_owned()], code_verifier: None, + pkce: None, serializer: OAuthSerializer::new(), }) } @@ -93,6 +98,10 @@ impl OpenIdCredential { pub fn authorization_url_builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { OpenIdAuthorizationUrlBuilder::new() } + + pub fn pkce(&self) -> Option<&ProofKeyForCodeExchange> { + self.pkce.as_ref() + } } #[async_trait] @@ -227,13 +236,33 @@ impl OpenIdCredentialBuilder { authorization_code: None, refresh_token: None, client_secret: String::new(), - scope: vec![], + scope: vec!["openid".to_owned()], code_verifier: None, + pkce: None, serializer: OAuthSerializer::new(), }, } } + pub(crate) fn new_with_auth_code_and_secret( + authorization_code: impl AsRef<str>, + client_secret: impl AsRef<str>, + app_config: AppConfig, + ) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder { + credential: OpenIdCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: client_secret.as_ref().to_owned(), + scope: vec![], + code_verifier: None, + pkce: None, + serializer: Default::default(), + }, + } + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self.credential.refresh_token = None; @@ -270,6 +299,13 @@ impl OpenIdCredentialBuilder { self } + pub fn generate_pkce(&mut self) -> AuthorizationResult<&mut Self> { + let pkce = ProofKeyForCodeExchange::generate()?; + self.with_code_verifier(pkce.code_verifier.as_str()); + self.credential.pkce = Some(pkce); + Ok(self) + } + pub fn credential(&self) -> &OpenIdCredential { &self.credential } diff --git a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs index ec696f67..fc4af66b 100644 --- a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs +++ b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs @@ -1,4 +1,5 @@ use crate::oauth::Crypto; +use graph_error::AuthorizationResult; #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct ProofKeyForCodeExchange { @@ -49,7 +50,7 @@ impl ProofKeyForCodeExchange { /// generate a secure random 32-octet sequence that is base64 URL /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn generate() -> anyhow::Result<ProofKeyForCodeExchange> { + pub fn generate() -> AuthorizationResult<ProofKeyForCodeExchange> { let (code_verifier, code_challenge) = Crypto::sha256_secure_string()?; Ok(ProofKeyForCodeExchange { code_verifier, diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index e0c18465..7cfc9ebb 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -52,7 +52,7 @@ impl ResourceOwnerPasswordCredential { password: T, ) -> ResourceOwnerPasswordCredential { ResourceOwnerPasswordCredential { - app_config: AppConfig::init(tenant_id.as_ref(), client_id), + app_config: AppConfig::new_with_tenant_and_client_id(tenant_id.as_ref(), client_id), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), scope: vec![], @@ -69,7 +69,7 @@ impl ResourceOwnerPasswordCredential { impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.app_config.authority); + .authority(&azure_authority_host, &self.app_config.authority); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 561f4d06..c03f3c6f 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -3,6 +3,7 @@ use crate::identity::{Authority, AzureCloudInstance}; use async_trait::async_trait; use graph_error::AuthorizationResult; +use http::header::ACCEPT; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; @@ -44,14 +45,66 @@ pub trait TokenCredentialExecutor { fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn client_id(&self) -> &String; fn authority(&self) -> Authority; + fn azure_cloud_instance(&self) -> AzureCloudInstance { AzureCloudInstance::AzurePublic } + fn basic_auth(&self) -> Option<(String, String)> { None } + fn app_config(&self) -> &AppConfig; + fn openid_configuration_url(&self) -> AuthorizationResult<Url> { + Ok(Url::parse( + format!( + "{}/{}/2.0/.well-known/openid-configuration", + self.azure_cloud_instance().as_ref(), + self.authority().as_ref() + ) + .as_str(), + )?) + } + + fn get_openid_config(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + let open_id_url = self.openid_configuration_url()?; + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + let response = http_client + .get(open_id_url) + .headers(headers) + .send() + .expect("Error on header"); + + Ok(response) + } + + async fn get_openid_config_async(&mut self) -> anyhow::Result<reqwest::Response> { + let open_id_config_url = self.openid_configuration_url()?; + let http_client = ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + let response = http_client + .get(open_id_config_url) + .headers(headers) + .send() + .await?; + + println!("{:#?}", response); + + Ok(response) + } + fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { let options = self.azure_cloud_instance(); let uri = self.uri(&options)?; @@ -112,3 +165,36 @@ pub trait TokenCredentialExecutor { } } } + +#[cfg(test)] +mod test { + use super::*; + use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; + + #[test] + fn open_id_configuration_url_authority_tenant_id() { + let open_id = ConfidentialClientApplicationBuilder::new("client-id") + .with_open_id("auth-code", "client-secret") + .with_tenant("tenant-id") + .build(); + + let url = open_id.openid_configuration_url().unwrap(); + assert_eq!( + "https://login.microsoftonline.com/tenant-id/2.0/.well-known/openid-configuration", + url.as_str() + ) + } + + #[test] + fn open_id_configuration_url_authority_common() { + let open_id = ConfidentialClientApplicationBuilder::new("client-id") + .with_open_id("auth-code", "client-secret") + .build(); + + let url = open_id.openid_configuration_url().unwrap(); + assert_eq!( + "https://login.microsoftonline.com/common/2.0/.well-known/openid-configuration", + url.as_str() + ) + } +} diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index e93f1154..4e8672c0 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -123,7 +123,7 @@ impl OAuthTestCredentials { fn client_credentials(self) -> ClientSecretCredential { let mut builder = ConfidentialClientApplication::builder(self.client_id.as_str()) - .with_client_secret_credential(self.client_secret.as_str()); + .with_client_secret(self.client_secret.as_str()); builder .with_tenant(self.tenant.as_str()) From b7545dcebffd2f152c477c3cb5dfdb9818db7e3b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 2 Sep 2023 21:52:43 -0400 Subject: [PATCH 034/118] Add extensions crate for crossover utilities --- Cargo.toml | 3 +- examples/oauth/main.rs | 4 +- examples/oauth/openid_connect/mod.rs | 3 + .../openid_local_and_server_auth.rs} | 15 ++-- graph-extensions/Cargo.toml | 27 ++++++ graph-extensions/src/http/http_ext.rs | 53 ++++++++++++ graph-extensions/src/http/mod.rs | 5 ++ .../src/http/response_converter.rs | 0 graph-extensions/src/lib.rs | 1 + .../credentials/application_builder.rs | 12 ++- .../credentials/device_code_credential.rs | 57 +++++++++---- .../credentials/open_id_authorization_url.rs | 82 +++++++------------ .../credentials/open_id_credential.rs | 9 +- .../credentials/token_credential_executor.rs | 4 +- 14 files changed, 183 insertions(+), 92 deletions(-) create mode 100644 examples/oauth/openid_connect/mod.rs rename examples/oauth/{open_id_connect.rs => openid_connect/openid_local_and_server_auth.rs} (85%) create mode 100644 graph-extensions/Cargo.toml create mode 100644 graph-extensions/src/http/http_ext.rs create mode 100644 graph-extensions/src/http/mod.rs create mode 100644 graph-extensions/src/http/response_converter.rs create mode 100644 graph-extensions/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 72289a17..cd74d5a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,8 @@ members = [ "test-tools", "graph-codegen", "graph-http", - "graph-core" + "graph-core", + "graph-extensions" ] [dependencies] diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index d6004530..7502a860 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -20,7 +20,7 @@ mod client_credentials; mod device_code; mod environment_credential; mod is_access_token_expired; -mod open_id_connect; +mod openid_connect; mod signing_keys; use graph_rs_sdk::oauth::{ @@ -32,7 +32,7 @@ use graph_rs_sdk::oauth::{ #[tokio::main] async fn main() { - open_id_connect::start_server_main().await; + } /* diff --git a/examples/oauth/openid_connect/mod.rs b/examples/oauth/openid_connect/mod.rs new file mode 100644 index 00000000..cb19e577 --- /dev/null +++ b/examples/oauth/openid_connect/mod.rs @@ -0,0 +1,3 @@ +mod openid_local_and_server_auth; + +pub use openid_local_and_server_auth::*; diff --git a/examples/oauth/open_id_connect.rs b/examples/oauth/openid_connect/openid_local_and_server_auth.rs similarity index 85% rename from examples/oauth/open_id_connect.rs rename to examples/oauth/openid_connect/openid_local_and_server_auth.rs index 4b5fa2e9..f9dec5f0 100644 --- a/examples/oauth/open_id_connect.rs +++ b/examples/oauth/openid_connect/openid_local_and_server_auth.rs @@ -27,13 +27,12 @@ static CLIENT_ID: &str = ""; static CLIENT_SECRET: &str = ""; static TENANT_ID: &str = ""; -fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { - Ok(OpenIdCredential::authorization_url_builder()? - .with_client_id(client_id) +fn openid_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { + Ok(ConfidentialClientApplication::builder(client_id) + .with_client_secret(client_secret) .with_response_mode(ResponseMode::FormPost) .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) - .with_default_scope()? .extend_scope(vec!["User.Read", "User.ReadWrite"]) .build() .url()?) @@ -45,10 +44,8 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let code = id_token.code.unwrap(); - let mut confidential_client = OpenIdCredential::builder() - .with_authorization_code(id_token.code) - .with_client_id(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_openid(id_token.code, CLIENT_SECRET) .with_tenant(TENANT_ID) .with_redirect_uri("http://localhost:8000/redirect")? .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope @@ -88,7 +85,7 @@ pub async fn start_server_main() { .and_then(handle_redirect) .with(warp::trace::named("executor")); - let url = open_id_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); + let url = openid_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); webbrowser::open(url.as_ref()); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml new file mode 100644 index 00000000..f1f7e381 --- /dev/null +++ b/graph-extensions/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "graph-extensions" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/sreeise/graph-rs-sdk" +description = "Extensions and utilities used across multiple crates that make up the graph-rs-sdk crate" + +[dependencies] +async-stream = "0.3" +async-trait = "0.1.35" +bytes = { version = "1.4.0", features = ["serde"] } +futures = "0.3.28" +http = "0.2.9" +reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.27.0", features = ["full"] } +url = { version = "2", features = ["serde"] } + +[features] +default = ["native-tls"] +native-tls = ["reqwest/native-tls"] +rustls-tls = ["reqwest/rustls-tls"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] diff --git a/graph-extensions/src/http/http_ext.rs b/graph-extensions/src/http/http_ext.rs new file mode 100644 index 00000000..2c9dadbc --- /dev/null +++ b/graph-extensions/src/http/http_ext.rs @@ -0,0 +1,53 @@ +use url::Url; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtUrl(pub Url); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtSerdeJsonValue(pub serde_json::Value); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtVecU8(pub Vec<u8>); + +/// Extension trait for http::response::Builder objects +/// +/// Allows the user to add a `Url` to the http::Response +pub trait HttpResponseBuilderExt { + /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` + /// to the `http::Response` + fn url(self, url: Url) -> Self; + fn json(self, value: &serde_json::Value) -> Self; +} + +impl HttpResponseBuilderExt for http::response::Builder { + fn url(self, url: Url) -> Self { + self.extension(HttpExtUrl(url)) + } + + fn json(self, value: &serde_json::Value) -> Self { + if let Ok(value) = serde_json::to_vec(value) { + return self.extension(HttpExtVecU8(value)); + } + + self + } +} + +pub trait HttpResponseExt { + fn url(&self) -> Option<Url>; + fn json(&self) -> Option<serde_json::Value>; +} + +impl<T> HttpResponseExt for http::Response<T> { + fn url(&self) -> Option<Url> { + self.extensions() + .get::<HttpExtUrl>() + .map(|url| url.clone().0) + } + + fn json(&self) -> Option<serde_json::Value> { + self.extensions() + .get::<HttpExtVecU8>() + .and_then(|value| serde_json::from_slice(value.0.as_slice()).ok()) + } +} diff --git a/graph-extensions/src/http/mod.rs b/graph-extensions/src/http/mod.rs new file mode 100644 index 00000000..2fe9350e --- /dev/null +++ b/graph-extensions/src/http/mod.rs @@ -0,0 +1,5 @@ +mod http_ext; +mod response_converter; + +pub use http_ext::*; +pub use response_converter::*; diff --git a/graph-extensions/src/http/response_converter.rs b/graph-extensions/src/http/response_converter.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs new file mode 100644 index 00000000..3883215f --- /dev/null +++ b/graph-extensions/src/lib.rs @@ -0,0 +1 @@ +pub mod http; diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 7ca3d17e..884bb5b5 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -108,11 +108,17 @@ impl ConfidentialClientApplicationBuilder { self } - pub fn get_authorization_request_url(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { + pub fn authorization_code_url_builder(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new() } - pub fn get_client_credential_request_url( + pub fn client_credentials_auth_url_builder( + &mut self, + ) -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new() + } + + pub fn openid_authorization_url_builder( &mut self, ) -> ClientCredentialsAuthorizationUrlBuilder { ClientCredentialsAuthorizationUrlBuilder::new() @@ -175,7 +181,7 @@ impl ConfidentialClientApplicationBuilder { ) } - pub fn with_open_id( + pub fn with_openid( self, authorization_code: impl AsRef<str>, client_secret: impl AsRef<str>, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4c745f0d..4799d32a 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,13 +1,42 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use crate::oauth::{DeviceCode, PublicClientApplication}; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationFailure, AuthorizationResult, AF, GraphFailure, GraphResult}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; +use std::time::Duration; +use anyhow::anyhow; use crate::identity::credentials::app_config::AppConfig; use url::Url; +/* +fn response_to_http_response(response: reqwest::Response) -> anyhow::Result<http::Response<>> { + let status = response.status(); + let url = response.url().clone(); + let headers = response.headers().clone(); + let version = response.version(); + + let body: serde_json::Value = response.json().await?; + let next_link = body.odata_next_link(); + let json = body.clone(); + let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) + .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); + + let mut builder = http::Response::builder() + .url(url) + .json(&json) + .status(http::StatusCode::from(&status)) + .version(version); + + for builder_header in builder.headers_mut().iter_mut() { + builder_header.extend(headers.clone()); + } + + Ok(builder.body(body_result)) +} + */ + const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; credential_builder!(DeviceCodeCredentialBuilder, PublicClientApplication); @@ -65,28 +94,27 @@ impl DeviceCodeCredential { self } - /* - pub async fn poll_async(&mut self, buffer: Option<usize>) -> tokio::sync::mpsc::Receiver<GraphResult<http::Response<serde_json::Value>>> { +/* + pub async fn poll_async(&mut self, buffer: Option<usize>) -> tokio::sync::mpsc::Receiver<anyhow::Result<http::Response<serde_json::Value>>> { let (sender, receiver) = { - if let Some(buffer) = buffer { - tokio::sync::mpsc::channel(buffer) - } else { - tokio::sync::mpsc::channel(100) - } + if let Some(buffer) = buffer { + tokio::sync::mpsc::channel(buffer) + } else { + tokio::sync::mpsc::channel(100) + } }; let mut credential = self.clone(); let mut application = PublicClientApplication::from(self.clone()); tokio::spawn(async move { - let response = application.get_token_async().await - .map_err(GraphFailure::from); + let response = application.execute_async().await.map_err(|err| anyhow!(err)); match response { Ok(response) => { let status = response.status(); - let body: serde_json::Value = response.json().await?; + let body: serde_json::Value = response.json().await.unwrap(); println!("{body:#?}"); let device_code = body["device_code"].as_str().unwrap(); @@ -100,8 +128,7 @@ impl DeviceCodeCredential { // Wait the amount of seconds that interval is. std::thread::sleep(Duration::from_secs(interval.clone())); - let response = application.get_token_async().await - .map_err(GraphFailure::from).unwrap(); + let response = application.execute_async().await.unwrap(); let status = response.status(); println!("{response:#?}"); @@ -133,14 +160,14 @@ impl DeviceCodeCredential { } } Err(err) => { - sender.send_timeout(Err(err), Duration::from_secs(60)); + sender.send_timeout(Err(err), Duration::from_secs(60)).await; } } }); return receiver; } - */ + */ pub fn builder() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new() diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index e1ed19f6..47a4a5db 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -5,8 +5,10 @@ use crate::identity::{ }; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use std::collections::BTreeSet; +use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; +use crate::identity::credentials::app_config::AppConfig; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your @@ -14,16 +16,7 @@ use url::Url; /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc #[derive(Clone, Debug)] pub struct OpenIdAuthorizationUrl { - /// Required - /// The Application (client) ID that the Azure portal – App registrations experience - /// assigned to your app. - pub(crate) client_id: String, - /// Required - /// The redirect URI of your app, where authentication responses can be sent and received - /// by your app. It must exactly match one of the redirect URIs you registered in the portal, - /// except that it must be URL-encoded. If not present, the endpoint will pick one registered - /// redirect_uri at random to send the user back to. - pub(crate) redirect_uri: Option<Url>, + pub(crate) app_config: AppConfig, /// Required /// Must include code for OpenID Connect sign-in. pub(crate) response_type: BTreeSet<ResponseType>, @@ -99,23 +92,30 @@ pub struct OpenIdAuthorizationUrl { /// this parameter during re-authentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, - pub(crate) authority: Authority, response_types_supported: Vec<String>, } impl OpenIdAuthorizationUrl { - pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( + pub fn new<T: AsRef<str>, IU: IntoUrl, U: ToString, I: IntoIterator<Item = U>>( client_id: T, - redirect_uri: Option<T>, + redirect_uri: IU, scope: I, ) -> AuthorizationResult<OpenIdAuthorizationUrl> { let mut scope_set = BTreeSet::new(); scope_set.insert("openid".to_owned()); scope_set.extend(scope.into_iter().map(|s| s.to_string())); + let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let mut open_id_url = OpenIdAuthorizationUrl { - client_id: client_id.as_ref().to_owned(), - redirect_uri: None, + Ok(OpenIdAuthorizationUrl { + app_config: AppConfig { + tenant_id: None, + client_id: client_id.as_ref().to_owned(), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + }, response_type: BTreeSet::new(), response_mode: None, nonce: Crypto::sha256_secure_string()?.1, @@ -124,20 +124,13 @@ impl OpenIdAuthorizationUrl { prompt: BTreeSet::new(), domain_hint: None, login_hint: None, - authority: Authority::default(), response_types_supported: vec![ "code".into(), "id_token".into(), "code id_token".into(), "id_token token".into(), ], - }; - - if let Some(redirect_uri) = redirect_uri.as_ref() { - open_id_url.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); - } - - Ok(open_id_url) + }) } pub fn builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { @@ -168,7 +161,7 @@ impl OpenIdAuthorizationUrl { impl AuthorizationUrl for OpenIdAuthorizationUrl { fn redirect_uri(&self) -> AuthorizationResult<Url> { - let redirect_uri = self.redirect_uri.as_ref() + let redirect_uri = self.app_config.redirect_uri.as_ref() .ok_or(AuthorizationFailure::msg_err( "redirect_uri", "If not provided, the authorization server will pick one registered redirect_uri at random to send the user back to" @@ -187,7 +180,8 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.as_str().trim(); + if client_id.is_empty() { return AuthorizationFailure::result("client_id"); } @@ -203,10 +197,10 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } serializer - .client_id(self.client_id.as_str()) + .client_id(client_id) .extend_scopes(self.scope.clone()) .nonce(self.nonce.as_str()) - .authority(azure_authority_host, &self.authority); + .authority(azure_authority_host, &self.app_config.authority); if self.response_type.is_empty() { serializer.response_type("code"); @@ -233,7 +227,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { serializer.response_mode(response_mode.as_ref()); } - if let Some(redirect_uri) = self.redirect_uri.as_ref() { + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { serializer.redirect_uri(redirect_uri.as_ref()); } @@ -292,8 +286,7 @@ impl OpenIdAuthorizationUrlBuilder { Ok(OpenIdAuthorizationUrlBuilder { auth_url_parameters: OpenIdAuthorizationUrl { - client_id: String::with_capacity(32), - redirect_uri: None, + app_config: AppConfig::default(), response_type: BTreeSet::new(), response_mode: None, nonce: Crypto::sha256_secure_string()?.1, @@ -302,7 +295,6 @@ impl OpenIdAuthorizationUrlBuilder { prompt: Default::default(), domain_hint: None, login_hint: None, - authority: Default::default(), response_types_supported: vec![ "code".into(), "id_token".into(), @@ -313,37 +305,27 @@ impl OpenIdAuthorizationUrlBuilder { }) } - pub fn new_with_secure_nonce(&mut self) -> anyhow::Result<OpenIdAuthorizationUrlBuilder> { - Ok(OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl::new( - String::with_capacity(32), - None, - BTreeSet::<String>::new(), - )?, - }) - } - pub fn with_redirect_uri<T: AsRef<str>>( &mut self, redirect_uri: T, ) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); + self.auth_url_parameters.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); Ok(self) } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.auth_url_parameters.client_id = client_id.as_ref().to_owned(); + self.auth_url_parameters.app_config.client_id = client_id.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.auth_url_parameters.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.auth_url_parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.auth_url_parameters.authority = authority.into(); + self.auth_url_parameters.app_config.authority = authority.into(); self } @@ -374,11 +356,6 @@ impl OpenIdAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - /* - if !response_mode.eq(&ResponseMode::Query) { - self.auth_url_parameters.response_mode = Some(response_mode); - } - */ self.auth_url_parameters.response_mode = Some(response_mode); self } @@ -504,8 +481,7 @@ mod test { #[test] #[should_panic] fn unsupported_response_type() { - let _ = OpenIdAuthorizationUrl::builder() - .unwrap() + let _ = OpenIdAuthorizationUrl::builder().unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) .with_client_id("client_id") .with_scope(["scope"]) diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 0f3bc812..2ff50812 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -314,13 +314,8 @@ impl OpenIdCredentialBuilder { impl From<OpenIdAuthorizationUrl> for OpenIdCredentialBuilder { fn from(value: OpenIdAuthorizationUrl) -> Self { let mut builder = OpenIdCredentialBuilder::new(); - if let Some(redirect_uri) = value.redirect_uri.as_ref() { - let _ = builder.with_redirect_uri(redirect_uri.clone()); - } - builder - .with_scope(value.scope) - .with_client_secret(value.client_id) - .with_authority(value.authority); + builder.credential.app_config = value.app_config; + builder.with_scope(value.scope); builder } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index c03f3c6f..6440c65c 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -174,7 +174,7 @@ mod test { #[test] fn open_id_configuration_url_authority_tenant_id() { let open_id = ConfidentialClientApplicationBuilder::new("client-id") - .with_open_id("auth-code", "client-secret") + .with_openid("auth-code", "client-secret") .with_tenant("tenant-id") .build(); @@ -188,7 +188,7 @@ mod test { #[test] fn open_id_configuration_url_authority_common() { let open_id = ConfidentialClientApplicationBuilder::new("client-id") - .with_open_id("auth-code", "client-secret") + .with_openid("auth-code", "client-secret") .build(); let url = open_id.openid_configuration_url().unwrap(); From 2f02f2c22a6cf5add4fbb0c3c91329cb7fab3d13 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 3 Sep 2023 05:03:24 -0400 Subject: [PATCH 035/118] Update device code authorization polling --- Cargo.toml | 11 +- examples/oauth/device_code.rs | 1 + examples/oauth/main.rs | 4 +- .../openid_local_and_server_auth.rs | 43 +++- graph-error/src/authorization_failure.rs | 11 + graph-error/src/lib.rs | 1 + graph-extensions/Cargo.toml | 2 + .../src/http/response_converter.rs | 61 +++++ graph-http/Cargo.toml | 1 + graph-http/src/lib.rs | 1 + graph-http/src/traits/http_ext.rs | 53 ----- graph-http/src/traits/mod.rs | 2 - graph-oauth/Cargo.toml | 1 + graph-oauth/src/access_token.rs | 18 +- graph-oauth/src/auth.rs | 15 ++ graph-oauth/src/auth_response_query.rs | 42 ---- graph-oauth/src/device_code.rs | 34 --- .../src/identity/application_options.rs | 4 +- .../identity/authorization_query_response.rs | 95 ++++++++ .../src/identity/authorization_serializer.rs | 6 +- .../credentials/application_builder.rs | 84 ++++++- .../auth_code_authorization_url_parameters.rs | 140 +++++------ ...thorization_code_certificate_credential.rs | 7 +- .../authorization_code_credential.rs | 7 +- .../confidential_client_application.rs | 6 +- .../credentials/device_code_credential.rs | 225 ++++++++++-------- graph-oauth/src/identity/credentials/mod.rs | 2 - .../credentials/open_id_authorization_url.rs | 22 +- .../credentials/public_client_application.rs | 15 +- .../public_client_application_builder.rs | 10 - .../resource_owner_password_credential.rs | 2 +- .../application_options/aad_options.json | 0 .../credentials/token_credential_executor.rs | 39 +-- graph-oauth/src/identity/device_code.rs | 105 ++++++++ graph-oauth/src/identity/mod.rs | 4 + graph-oauth/src/lib.rs | 25 +- graph-oauth/src/oauth2_header.rs | 1 - src/lib.rs | 6 +- 38 files changed, 670 insertions(+), 436 deletions(-) delete mode 100644 graph-http/src/traits/http_ext.rs delete mode 100644 graph-oauth/src/auth_response_query.rs delete mode 100644 graph-oauth/src/device_code.rs create mode 100644 graph-oauth/src/identity/authorization_query_response.rs delete mode 100644 graph-oauth/src/identity/credentials/public_client_application_builder.rs rename {test_files => graph-oauth/src/identity/credentials/test}/application_options/aad_options.json (100%) create mode 100644 graph-oauth/src/identity/device_code.rs delete mode 100644 graph-oauth/src/oauth2_header.rs diff --git a/Cargo.toml b/Cargo.toml index cd74d5a6..8deeb5ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ graph-oauth = { path = "./graph-oauth", version = "1.0.2", default-features=fals graph-http = { path = "./graph-http", version = "1.1.0", default-features=false } graph-error = { path = "./graph-error", version = "0.2.2" } graph-core = { path = "./graph-core", version = "0.4.0" } +graph-extensions = { path = "./graph-extensions", version = "0.1.0", default-features=false } # When updating or adding new features to this or dependent crates run # cargo tree -e features -i graph-rs-sdk @@ -50,11 +51,11 @@ graph-core = { path = "./graph-core", version = "0.4.0" } [features] default = ["native-tls"] -native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls"] -rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls"] -brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli"] -deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate"] -trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns"] +native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls", "graph-extensions/native-tls"] +rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls", "graph-extensions/rustls-tls"] +brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli", "graph-extensions/brotli"] +deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate", "graph-extensions/deflate"] +trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns", "graph-extensions/trust-dns"] openssl = ["graph-oauth/openssl"] [dev-dependencies] diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 9d2078c6..a714a958 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,4 +1,5 @@ use graph_oauth::identity::{DeviceCodeCredential, TokenCredentialExecutor}; +use graph_oauth::oauth::DeviceCodeCredentialBuilder; use graph_rs_sdk::oauth::{MsalTokenResponse, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 7502a860..25268c95 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -31,9 +31,7 @@ use graph_rs_sdk::oauth::{ }; #[tokio::main] -async fn main() { - -} +async fn main() {} /* // Some examples of what you can use for authentication and getting access tokens. There are diff --git a/examples/oauth/openid_connect/openid_local_and_server_auth.rs b/examples/oauth/openid_connect/openid_local_and_server_auth.rs index f9dec5f0..0e12f0f4 100644 --- a/examples/oauth/openid_connect/openid_local_and_server_auth.rs +++ b/examples/oauth/openid_connect/openid_local_and_server_auth.rs @@ -4,6 +4,7 @@ use graph_oauth::identity::{ }; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; use graph_rs_sdk::oauth::{IdToken, MsalTokenResponse, OAuthSerializer}; +use tracing_subscriber::fmt::format::FmtSpan; use url::Url; /// # Example @@ -27,15 +28,21 @@ static CLIENT_ID: &str = ""; static CLIENT_SECRET: &str = ""; static TENANT_ID: &str = ""; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + fn openid_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { - Ok(ConfidentialClientApplication::builder(client_id) - .with_client_secret(client_secret) - .with_response_mode(ResponseMode::FormPost) - .with_response_type([ResponseType::IdToken, ResponseType::Code]) - .with_prompt(Prompt::SelectAccount) - .extend_scope(vec!["User.Read", "User.ReadWrite"]) - .build() - .url()?) + Ok(OpenIdCredential::authorization_url_builder()? + .with_client_id(CLIENT_ID) + .with_tenant(TENANT_ID) + //.with_default_scope()? + .with_redirect_uri("http://localhost:8000/redirect")? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state(REDIRECT_URI) + .extend_scope(vec!["User.Read", "User.ReadWrite"]) + .build() + .url()?) } async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, warp::Rejection> { @@ -45,9 +52,9 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let code = id_token.code.unwrap(); let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_openid(id_token.code, CLIENT_SECRET) + .with_openid(code, CLIENT_SECRET) .with_tenant(TENANT_ID) - .with_redirect_uri("http://localhost:8000/redirect")? + .with_redirect_uri(REDIRECT_URI).unwrap() .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope .build(); @@ -79,6 +86,22 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, /// } /// ``` pub async fn start_server_main() { + let filter = + std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=debug,warp=debug".to_owned()); + + // Configure the default `tracing` subscriber. + // The `fmt` subscriber from the `tracing-subscriber` crate logs `tracing` + // events to stdout. Other subscribers are available for integrating with + // distributed tracing systems such as OpenTelemetry. + tracing_subscriber::fmt() + .with_span_events(FmtSpan::FULL) + // Use the filter we built above to determine which traces to record. + .with_env_filter(filter) + // Record an event when each span closes. This can be used to time our + // routes' durations! + .with_span_events(FmtSpan::CLOSE) + .init(); + let routes = warp::post() .and(warp::path("redirect")) .and(warp::body::form()) diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 29f40641..bfa9d1da 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,5 +1,6 @@ use crate::AuthorizationResult; + pub type AF = AuthorizationFailure; #[derive(Debug, thiserror::Error)] @@ -80,3 +81,13 @@ impl AuthorizationFailure { } } } + +#[derive(Debug, thiserror::Error)] +pub enum AuthExecutionError { + #[error("{0:#?}")] + AuthorizationFailure(#[from] AuthorizationFailure), + #[error("{0:#?}")] + RequestError(#[from] reqwest::Error), + #[error("{0:#?}")] + SerdeError(#[from] serde_json::error::Error), +} diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index c47d3597..2429c8d7 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -17,3 +17,4 @@ pub use internal::*; pub type GraphResult<T> = Result<T, GraphFailure>; pub type AuthorizationResult<T> = Result<T, AuthorizationFailure>; +pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml index f1f7e381..5bac93ff 100644 --- a/graph-extensions/Cargo.toml +++ b/graph-extensions/Cargo.toml @@ -18,6 +18,8 @@ serde_json = "1" tokio = { version = "1.27.0", features = ["full"] } url = { version = "2", features = ["serde"] } +graph-error = { path = "../graph-error" } + [features] default = ["native-tls"] native-tls = ["reqwest/native-tls"] diff --git a/graph-extensions/src/http/response_converter.rs b/graph-extensions/src/http/response_converter.rs index e69de29b..394c84b5 100644 --- a/graph-extensions/src/http/response_converter.rs +++ b/graph-extensions/src/http/response_converter.rs @@ -0,0 +1,61 @@ +use crate::http::HttpResponseBuilderExt; +use async_trait::async_trait; +use graph_error::{ErrorMessage, GraphResult}; +use http::Response; +use serde::de::DeserializeOwned; + +/* +pub async fn into_http_response_async<T: DeserializeOwned>(response: reqwest::Response) -> GraphResult<http::Response<Result<T, ErrorMessage>>> { + let status = response.status(); + let url = response.url().clone(); + let headers = response.headers().clone(); + let version = response.version(); + + let body: serde_json::Value = response.json().await?; + let json = body.clone(); + + let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) + .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); + + let mut builder = http::Response::builder() + .url(url) + .json(&json) + .status(http::StatusCode::from(&status)) + .version(version); + + Ok(builder.body(body_result)?) +} + */ + +#[async_trait] +pub trait ResponseConverterExt { + async fn into_json_http_response_async<T: DeserializeOwned>( + self, + ) -> GraphResult<http::Response<Result<T, ErrorMessage>>>; +} + +#[async_trait] +impl ResponseConverterExt for reqwest::Response { + async fn into_json_http_response_async<T: DeserializeOwned>( + self, + ) -> GraphResult<Response<Result<T, ErrorMessage>>> { + let status = self.status(); + let url = self.url().clone(); + let _headers = self.headers().clone(); + let version = self.version(); + + let body: serde_json::Value = self.json().await?; + let json = body.clone(); + + let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) + .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); + + let builder = http::Response::builder() + .url(url) + .json(&json) + .status(http::StatusCode::from(&status)) + .version(version); + + Ok(builder.body(body_result)?) + } +} diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 2336f1c7..111952e6 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -25,6 +25,7 @@ url = { version = "2", features = ["serde"] } graph-error = { path = "../graph-error" } graph-core = { path = "../graph-core" } +graph-extensions = { path = "../graph-extensions" } [features] default = ["native-tls"] diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 93c5ffd0..6f49d993 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -28,6 +28,7 @@ pub(crate) mod internal { pub use crate::traits::*; pub use crate::upload_session::*; pub use crate::url::*; + pub use graph_extensions::http::*; } pub mod api_impl { diff --git a/graph-http/src/traits/http_ext.rs b/graph-http/src/traits/http_ext.rs deleted file mode 100644 index 2c9dadbc..00000000 --- a/graph-http/src/traits/http_ext.rs +++ /dev/null @@ -1,53 +0,0 @@ -use url::Url; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtUrl(pub Url); - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtSerdeJsonValue(pub serde_json::Value); - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtVecU8(pub Vec<u8>); - -/// Extension trait for http::response::Builder objects -/// -/// Allows the user to add a `Url` to the http::Response -pub trait HttpResponseBuilderExt { - /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` - /// to the `http::Response` - fn url(self, url: Url) -> Self; - fn json(self, value: &serde_json::Value) -> Self; -} - -impl HttpResponseBuilderExt for http::response::Builder { - fn url(self, url: Url) -> Self { - self.extension(HttpExtUrl(url)) - } - - fn json(self, value: &serde_json::Value) -> Self { - if let Ok(value) = serde_json::to_vec(value) { - return self.extension(HttpExtVecU8(value)); - } - - self - } -} - -pub trait HttpResponseExt { - fn url(&self) -> Option<Url>; - fn json(&self) -> Option<serde_json::Value>; -} - -impl<T> HttpResponseExt for http::Response<T> { - fn url(&self) -> Option<Url> { - self.extensions() - .get::<HttpExtUrl>() - .map(|url| url.clone().0) - } - - fn json(&self) -> Option<serde_json::Value> { - self.extensions() - .get::<HttpExtVecU8>() - .and_then(|value| serde_json::from_slice(value.0.as_slice()).ok()) - } -} diff --git a/graph-http/src/traits/mod.rs b/graph-http/src/traits/mod.rs index 47cb3eb2..39b1c5d6 100644 --- a/graph-http/src/traits/mod.rs +++ b/graph-http/src/traits/mod.rs @@ -3,7 +3,6 @@ mod async_iterator; mod async_try_from; mod body_ext; mod byte_range; -mod http_ext; mod odata_link; mod odata_query; mod response_blocking_ext; @@ -14,7 +13,6 @@ pub use async_iterator::*; pub use async_try_from::*; pub use body_ext::*; pub use byte_range::*; -pub use http_ext::*; pub use odata_link::*; pub use odata_query::*; pub use response_blocking_ext::*; diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 875c58a2..ba20c8a9 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -43,6 +43,7 @@ hyper = { version = "1.0.0-rc.3", features = ["full"] } http-body-util = "0.1.0-rc.2" graph-error = { path = "../graph-error" } +graph-extensions = { path = "../graph-extensions" } [features] default = ["native-tls"] diff --git a/graph-oauth/src/access_token.rs b/graph-oauth/src/access_token.rs index ea7cec05..b1519f12 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-oauth/src/access_token.rs @@ -383,7 +383,7 @@ impl TryFrom<reqwest::blocking::Response> for MsalTokenResponse { impl fmt::Debug for MsalTokenResponse { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.log_pii { - f.debug_struct("AccessToken") + f.debug_struct("MsalAccessToken") .field("bearer_token", &self.access_token) .field("refresh_token", &self.refresh_token) .field("token_type", &self.token_type) @@ -396,13 +396,23 @@ impl fmt::Debug for MsalTokenResponse { .field("additional_fields", &self.additional_fields) .finish() } else { - f.debug_struct("AccessToken") - .field("bearer_token", &"[REDACTED]") + f.debug_struct("MsalAccessToken") + .field( + "bearer_token", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) + .field( + "refresh_token", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) .field("token_type", &self.token_type) .field("expires_in", &self.expires_in) .field("scope", &self.scope) .field("user_id", &self.user_id) - .field("id_token", &"[REDACTED]") + .field( + "id_token", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) .field("state", &self.state) .field("timestamp", &self.timestamp) .field("additional_fields", &self.additional_fields) diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 0c205e98..bbdb5a92 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -404,6 +404,21 @@ impl OAuthSerializer { self.authorization_url(&auth_url).token_uri(&token_url) } + pub fn authority_device_code( + &mut self, + host: &AzureCloudInstance, + authority: &Authority, + ) -> &mut OAuthSerializer { + let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); + let auth_url = format!( + "{}/{}/oauth2/v2.0/devicecode", + host.as_ref(), + authority.as_ref() + ); + + self.authorization_url(&auth_url).token_uri(&token_url) + } + pub fn legacy_authority(&mut self) -> &mut OAuthSerializer { let url = "https://login.live.com/oauth20_desktop.srf".to_string(); self.authorization_url(url.as_str()); diff --git a/graph-oauth/src/auth_response_query.rs b/graph-oauth/src/auth_response_query.rs deleted file mode 100644 index 307a32cd..00000000 --- a/graph-oauth/src/auth_response_query.rs +++ /dev/null @@ -1,42 +0,0 @@ -use serde_json::Value; -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; - -#[derive(Clone, Serialize, Deserialize)] -pub struct AuthQueryResponse { - pub code: Option<String>, - pub id_token: Option<String>, - pub access_token: Option<String>, - pub state: Option<String>, - pub nonce: Option<String>, - pub error: Option<String>, - pub error_description: Option<String>, - #[serde(flatten)] - pub additional_fields: HashMap<String, Value>, - #[serde(skip)] - log_pii: bool, -} - -impl Debug for AuthQueryResponse { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.log_pii { - f.debug_struct("AuthQueryResponse") - .field("code", &self.code) - .field("id_token", &self.id_token) - .field("access_token", &self.access_token) - .field("state", &self.state) - .field("nonce", &self.nonce) - .field("additional_fields(serde flatten)", &self.additional_fields) - .finish() - } else { - f.debug_struct("AuthQueryResponse") - .field("code", &self.code) - .field("id_token", &"[REDACTED]") - .field("access_token", &"[REDACTED]") - .field("state", &self.state) - .field("nonce", &self.nonce) - .field("additional_fields(serde flatten)", &self.additional_fields) - .finish() - } - } -} diff --git a/graph-oauth/src/device_code.rs b/graph-oauth/src/device_code.rs deleted file mode 100644 index 320f41a8..00000000 --- a/graph-oauth/src/device_code.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde_json::Value; -use std::collections::{BTreeSet, HashMap}; -use std::time::Duration; - -/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct DeviceCode { - /// A long string used to verify the session between the client and the authorization server. - /// The client uses this parameter to request the access token from the authorization server. - pub device_code: String, - /// The number of seconds before the device_code and user_code expire. - pub expires_in: u64, - /// OPTIONAL - /// The minimum amount of time in seconds that the client - /// SHOULD wait between polling requests to the token endpoint. If no - /// value is provided, clients MUST use 5 as the default. - #[serde(default = "default_interval")] - pub interval: Option<Duration>, - /// User friendly text response that can be used for display purpose. - pub message: String, - pub user_code: String, - /// Verification URL where the user must navigate to authenticate using the device code - /// and credentials. - pub verification_uri: String, - pub verification_uri_complete: Option<String>, - /// List of the scopes that would be held by token. - pub scopes: Option<BTreeSet<String>>, - #[serde(flatten)] - pub additional_fields: HashMap<String, Value>, -} - -fn default_interval() -> Option<Duration> { - Some(Duration::from_secs(5)) -} diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index dd2487a1..1e9b5ca0 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -54,7 +54,9 @@ mod test { #[test] fn application_options_from_file() { - let file = File::open(r#"test_files\application_options\aad_options.json"#).unwrap(); + let file = + File::open(r#"./src/identity/credentials/test/application_options/aad_options.json"#) + .unwrap(); let application_options: ApplicationOptions = serde_json::from_reader(file).unwrap(); assert_eq!( application_options.aad_authority_audience, diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs new file mode 100644 index 00000000..f4602af2 --- /dev/null +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -0,0 +1,95 @@ +use serde_json::Value; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use url::Url; + +/// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-31#section-4.2.2.1 +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum AuthorizationQueryError { + /// The request is missing a required parameter, includes an + /// invalid parameter value, includes a parameter more than + /// once, or is otherwise malformed. + #[serde(alias = "invalid_request", alias = "InvalidRequest")] + InvalidRequest, + + /// The client is not authorized to request an access token + /// using this method. + #[serde(alias = "unauthorized_client", alias = "UnauthorizedClient")] + UnauthorizedClient, + + /// The resource owner or authorization server denied the + /// request. + #[serde(alias = "access_denied", alias = "AccessDenied")] + AccessDenied, + + /// The authorization server does not support obtaining an + /// access token using this method + #[serde(alias = "unsupported_response_type", alias = "UnsupportedResponseType")] + UnsupportedResponseType, + + /// The requested scope is invalid, unknown, or malformed. + #[serde(alias = "invalid_scope", alias = "InvalidScope")] + InvalidScope, + + /// The authorization server encountered an unexpected + /// condition that prevented it from fulfilling the request. + /// (This error code is needed because a 500 Internal Server + /// Error HTTP status code cannot be returned to the client + /// via a HTTP redirect.) + #[serde(alias = "server_error", alias = "ServerError")] + ServerError, + + /// The authorization server is currently unable to handle + /// the request due to a temporary overloading or maintenance + /// of the server. (This error code is needed because a 503 + /// Service Unavailable HTTP status code cannot be returned + /// to the client via a HTTP redirect.) + #[serde(alias = "temporarily_unavailable", alias = "TemporarilyUnavailable")] + TemporarilyUnavailable, +} + +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizationQueryResponse { + pub code: Option<String>, + pub id_token: Option<String>, + pub access_token: Option<String>, + pub state: Option<String>, + pub nonce: Option<String>, + pub error: Option<AuthorizationQueryError>, + pub error_description: Option<String>, + pub error_uri: Option<Url>, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, + #[serde(skip)] + log_pii: bool, +} + +impl Debug for AuthorizationQueryResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.log_pii { + f.debug_struct("AuthQueryResponse") + .field("code", &self.code) + .field("id_token", &self.id_token) + .field("access_token", &self.access_token) + .field("state", &self.state) + .field("nonce", &self.nonce) + .field("error", &self.error) + .field("error_description", &self.error_description) + .field("error_uri", &self.error_uri) + .field("additional_fields(serde flatten)", &self.additional_fields) + .finish() + } else { + f.debug_struct("AuthQueryResponse") + .field("code", &self.code) + .field("id_token", &"[REDACTED]") + .field("access_token", &"[REDACTED]") + .field("state", &self.state) + .field("nonce", &self.nonce) + .field("error", &self.error) + .field("error_description", &self.error_description) + .field("error_uri", &self.error_uri) + .field("additional_fields(serde flatten)", &self.additional_fields) + .finish() + } + } +} diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index dab2d99e..8e8df32f 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use url::Url; pub trait AuthorizationSerializer { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url>; fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; fn basic_auth(&self) -> Option<(String, String)> { None @@ -12,10 +12,10 @@ pub trait AuthorizationSerializer { } pub trait AuthorizationUrl { - fn redirect_uri(&self) -> AuthorizationResult<Url>; + fn redirect_uri(&self) -> Option<&Url>; fn authorization_url(&self) -> AuthorizationResult<Url>; fn authorization_url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url>; } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 884bb5b5..74b95979 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -4,13 +4,15 @@ use crate::identity::{ application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, + DeviceCodeCredentialBuilder, EnvironmentCredential, OpenIdCredentialBuilder, + PublicClientApplication, }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; -use crate::oauth::OpenIdCredentialBuilder; use graph_error::{AuthorizationResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; +use std::env::VarError; use url::Url; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -53,9 +55,7 @@ impl ConfidentialClientApplicationBuilder { pub fn new_with_application_options( application_options: ApplicationOptions, ) -> AuthorizationResult<ConfidentialClientApplicationBuilder> { - Ok(ConfidentialClientApplicationBuilder::try_from( - application_options, - )?) + ConfidentialClientApplicationBuilder::try_from(application_options) } pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { @@ -118,9 +118,7 @@ impl ConfidentialClientApplicationBuilder { ClientCredentialsAuthorizationUrlBuilder::new() } - pub fn openid_authorization_url_builder( - &mut self, - ) -> ClientCredentialsAuthorizationUrlBuilder { + pub fn openid_authorization_url_builder(&mut self) -> ClientCredentialsAuthorizationUrlBuilder { ClientCredentialsAuthorizationUrlBuilder::new() } @@ -257,9 +255,75 @@ impl PublicClientApplicationBuilder { pub fn create_with_application_options( application_options: ApplicationOptions, ) -> AuthorizationResult<PublicClientApplicationBuilder> { - Ok(PublicClientApplicationBuilder::try_from( - application_options, - )?) + PublicClientApplicationBuilder::try_from(application_options) + } + + pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + let tenant = tenant_id.as_ref().to_string(); + self.app_config.tenant_id = Some(tenant.clone()); + self.app_config.authority = Authority::TenantId(tenant); + self + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { + self.app_config + .extra_query_parameters + .insert(query_param.0, query_param.1); + self + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_query_parameters( + &mut self, + query_parameters: HashMap<String, String>, + ) -> &mut Self { + self.app_config + .extra_query_parameters + .extend(query_parameters); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_param<K: Into<HeaderName>, V: Into<HeaderValue>>( + &mut self, + header_name: K, + header_value: V, + ) -> &mut Self { + self.app_config + .extra_header_parameters + .insert(header_name.into(), header_value.into()); + self + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { + self.app_config + .extra_header_parameters + .extend(header_parameters); + self + } + + pub fn with_device_code_builder(self) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder::new_with_app_config(self.app_config) + } + + pub fn with_device_code(self, device_code: impl AsRef<str>) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder::new_with_device_code(device_code.as_ref(), self.app_config) + } + + /* + pub fn interactive_authentication(self) -> DeviceCodeCredentialBuilder { + + } + */ + + pub fn try_from_environment() -> Result<PublicClientApplication, VarError> { + EnvironmentCredential::resource_owner_password_credential() } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index 3be0bb8b..14426bcf 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -1,13 +1,15 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::auth_response_query::AuthQueryResponse; use crate::identity::{ - Authority, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, ResponseMode, + Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, + ResponseMode, }; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; use crate::web::{InteractiveAuthenticator, InteractiveWebViewOptions}; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationResult, AF}; +use crate::identity::credentials::app_config::AppConfig; +use reqwest::IntoUrl; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; @@ -27,10 +29,7 @@ use url::Url; /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code #[derive(Clone, Debug)] pub struct AuthCodeAuthorizationUrlParameters { - /// The client (application) ID of the service principal - pub(crate) client_id: String, - pub(crate) redirect_uri: String, - pub(crate) authority: Authority, + pub(crate) app_config: AppConfig, pub(crate) response_type: BTreeSet<ResponseType>, /// Optional /// Specifies how the identity platform should return the requested token to your app. @@ -56,13 +55,24 @@ pub struct AuthCodeAuthorizationUrlParameters { } impl AuthCodeAuthorizationUrlParameters { - pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> AuthCodeAuthorizationUrlParameters { + pub fn new<T: AsRef<str>, U: IntoUrl>( + client_id: T, + redirect_uri: U, + ) -> AuthorizationResult<AuthCodeAuthorizationUrlParameters> { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); - AuthCodeAuthorizationUrlParameters { - client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), - authority: Authority::default(), + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + + Ok(AuthCodeAuthorizationUrlParameters { + app_config: AppConfig { + tenant_id: None, + client_id: client_id.as_ref().to_owned(), + authority: Default::default(), + authority_url: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + }, response_type, response_mode: None, nonce: None, @@ -73,7 +83,7 @@ impl AuthCodeAuthorizationUrlParameters { login_hint: None, code_challenge: None, code_challenge_method: None, - } + }) } pub fn builder() -> AuthCodeAuthorizationUrlParameterBuilder { @@ -104,7 +114,7 @@ impl AuthCodeAuthorizationUrlParameters { pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<InteractiveWebViewOptions>, - ) -> anyhow::Result<AuthQueryResponse> { + ) -> anyhow::Result<AuthorizationQueryResponse> { let url_string = self .interactive_authentication(interactive_web_view_options)? .ok_or(anyhow::Error::msg( @@ -140,7 +150,7 @@ impl AuthCodeAuthorizationUrlParameters { &format!("No query or fragment returned on redirect, url: {url}"), ))?; - let response_query: AuthQueryResponse = serde_urlencoded::from_str(query)?; + let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; Ok(response_query) } } @@ -155,7 +165,7 @@ mod web_view_authenticator { interactive_web_view_options: Option<InteractiveWebViewOptions>, ) -> anyhow::Result<Option<String>> { let uri = self.authorization_url()?; - let redirect_uri = self.redirect_uri()?; + let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let _timeout = web_view_options.timeout; let (sender, receiver) = std::sync::mpsc::channel(); @@ -182,8 +192,8 @@ mod web_view_authenticator { } impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { - fn redirect_uri(&self) -> AuthorizationResult<Url> { - Url::parse(self.redirect_uri.as_str()).map_err(AuthorizationFailure::from) + fn redirect_uri(&self) -> Option<&Url> { + self.app_config.redirect_uri.as_ref() } fn authorization_url(&self) -> AuthorizationResult<Url> { @@ -192,15 +202,20 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { fn authorization_url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - if self.redirect_uri.trim().is_empty() { - return AF::result("redirect_uri"); + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { + if redirect_uri.as_str().trim().is_empty() { + return AF::result("redirect_uri"); + } else { + serializer.redirect_uri(redirect_uri.as_str()); + } } - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.trim(); + if client_id.is_empty() { return AF::result("client_id"); } @@ -216,10 +231,9 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { } serializer - .client_id(self.client_id.as_str()) - .redirect_uri(self.redirect_uri.as_str()) + .client_id(client_id) .extend_scopes(self.scope.clone()) - .authority(azure_authority_host, &self.authority); + .authority(azure_cloud_instance, &self.app_config.authority); let response_types: Vec<String> = self.response_type.iter().map(|s| s.to_string()).collect(); @@ -310,7 +324,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { #[derive(Clone)] pub struct AuthCodeAuthorizationUrlParameterBuilder { - authorization_url: AuthCodeAuthorizationUrlParameters, + parameters: AuthCodeAuthorizationUrlParameters, } impl Default for AuthCodeAuthorizationUrlParameterBuilder { @@ -324,10 +338,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { - authorization_url: AuthCodeAuthorizationUrlParameters { - client_id: String::with_capacity(32), - redirect_uri: String::new(), - authority: Authority::default(), + parameters: AuthCodeAuthorizationUrlParameters { + app_config: AppConfig::default(), response_mode: None, response_type, nonce: None, @@ -342,24 +354,24 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); + pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> &mut Self { + self.parameters.app_config.redirect_uri = Some(redirect_uri.into_url().unwrap()); self } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.authorization_url.client_id = client_id.as_ref().to_owned(); + self.parameters.app_config.client_id = client_id.as_ref().to_owned(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.authorization_url.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.authorization_url.authority = authority.into(); + self.parameters.app_config.authority = authority.into(); self } @@ -369,7 +381,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { &mut self, response_type: I, ) -> &mut Self { - self.authorization_url + self.parameters .response_type .extend(response_type.into_iter()); self @@ -387,7 +399,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.authorization_url.response_mode = Some(response_mode); + self.parameters.response_mode = Some(response_mode); self } @@ -396,7 +408,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.authorization_url.nonce = Some(nonce.as_ref().to_owned()); + self.parameters.nonce = Some(nonce.as_ref().to_owned()); self } @@ -413,12 +425,12 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.authorization_url.nonce = Some(Crypto::sha256_secure_string()?.1); + self.parameters.nonce = Some(Crypto::sha256_secure_string()?.1); Ok(self) } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.authorization_url.state = Some(state.as_ref().to_owned()); + self.parameters.state = Some(state.as_ref().to_owned()); self } @@ -428,18 +440,15 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// and generates a secure nonce value. /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.authorization_url.scope.extend( + self.parameters.scope.extend( scope .into_iter() .map(|s| s.to_string()) .map(|s| s.trim().to_owned()), ); - if self.authorization_url.nonce.is_none() - && self - .authorization_url - .scope - .contains(&String::from("id_token")) + if self.parameters.nonce.is_none() + && self.parameters.scope.contains(&String::from("id_token")) { let _ = self.with_id_token_scope(); } @@ -451,7 +460,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// If you need a refresh token then include `offline_access` as a scope. /// The `offline_access` scope is not included here. pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { - self.authorization_url + self.parameters .scope .extend(vec!["profile".to_owned(), "email".to_owned()]); Ok(self) @@ -460,7 +469,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// Adds the `offline_access` scope parameter which tells the authorization server /// to include a refresh token in the redirect uri query. pub fn with_refresh_token_scope(&mut self) -> &mut Self { - self.authorization_url + self.parameters .scope .extend(vec!["offline_access".to_owned()]); self @@ -477,12 +486,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { self.with_nonce_generated()?; - self.authorization_url - .response_type - .extend(ResponseType::IdToken); - self.authorization_url - .scope - .extend(vec!["id_token".to_owned()]); + self.parameters.response_type.extend(ResponseType::IdToken); + self.parameters.scope.extend(vec!["id_token".to_owned()]); Ok(self) } @@ -497,24 +502,24 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.authorization_url.prompt = Some(prompt); + self.parameters.prompt = Some(prompt); self } pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.authorization_url.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); self } pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.authorization_url.login_hint = Some(login_hint.as_ref().to_owned()); + self.parameters.login_hint = Some(login_hint.as_ref().to_owned()); self } /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). /// Required if code_challenge_method is included. pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { - self.authorization_url.code_challenge = Some(code_challenge.as_ref().to_owned()); + self.parameters.code_challenge = Some(code_challenge.as_ref().to_owned()); self } @@ -527,8 +532,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { &mut self, code_challenge_method: T, ) -> &mut Self { - self.authorization_url.code_challenge_method = - Some(code_challenge_method.as_ref().to_owned()); + self.parameters.code_challenge_method = Some(code_challenge_method.as_ref().to_owned()); self } @@ -545,11 +549,11 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { - self.authorization_url.clone() + self.parameters.clone() } pub fn url(&self) -> AuthorizationResult<Url> { - self.authorization_url.url() + self.parameters.url() } } @@ -560,7 +564,7 @@ mod test { #[test] fn serialize_uri() { let authorizer = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .build(); @@ -572,7 +576,7 @@ mod test { #[test] fn url_with_host() { let authorizer = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .build(); @@ -584,7 +588,7 @@ mod test { #[test] fn response_mode_set() { let url = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .with_response_type(ResponseType::IdToken) @@ -600,7 +604,7 @@ mod test { #[test] fn response_mode_not_set() { let url = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .url() @@ -614,7 +618,7 @@ mod test { #[test] fn multi_response_type_set() { let url = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) @@ -630,7 +634,7 @@ mod test { #[test] fn generate_nonce() { let url = AuthCodeAuthorizationUrlParameters::builder() - .with_redirect_uri("https::/localhost:8080") + .with_redirect_uri("https://localhost:8080") .with_client_id("client_id") .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 1c7ef007..715d41c8 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -354,11 +354,8 @@ impl AuthorizationCodeCertificateCredentialBuilder { impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCertificateCredentialBuilder { fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); - let _ = builder.with_redirect_uri(value.redirect_uri); - builder - .with_scope(value.scope) - .with_client_id(value.client_id) - .with_authority(value.authority); + builder.credential.app_config = value.app_config; + builder.with_scope(value.scope); builder } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index b8e0241a..c24c6b1e 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -223,11 +223,8 @@ impl AuthorizationCodeCredentialBuilder { impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCredentialBuilder { fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { let mut builder = AuthorizationCodeCredentialBuilder::new(); - let _ = builder.with_redirect_uri(value.redirect_uri); - builder - .with_scope(value.scope) - .with_client_id(value.client_id) - .with_authority(value.authority); + builder.credential.app_config = value.app_config; + builder.with_scope(value.scope); builder } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 4156348e..95dd1023 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -8,7 +8,7 @@ use crate::identity::{ }; use async_trait::async_trait; -use graph_error::AuthorizationResult; +use graph_error::{AuthExecutionResult, AuthorizationResult}; use reqwest::header::{HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; @@ -82,7 +82,7 @@ impl TokenCredentialExecutor for ConfidentialClientApplication { self.credential.app_config() } - fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let azure_cloud_instance = self.azure_cloud_instance(); let uri = self.credential.uri(&azure_cloud_instance)?; let form = self.credential.form_urlencode()?; @@ -110,7 +110,7 @@ impl TokenCredentialExecutor for ConfidentialClientApplication { } } - async fn execute_async(&mut self) -> anyhow::Result<Response> { + async fn execute_async(&mut self) -> AuthExecutionResult<Response> { let azure_cloud_instance = self.azure_cloud_instance(); let uri = self.credential.uri(&azure_cloud_instance)?; let form = self.credential.form_urlencode()?; diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4799d32a..4a58abc0 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,41 +1,19 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; -use crate::oauth::{DeviceCode, PublicClientApplication}; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF, GraphFailure, GraphResult}; +use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; + +use graph_error::{ + AuthExecutionError, AuthExecutionResult, AuthorizationFailure, AuthorizationResult, AF, +}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; +use std::ops::Add; +use std::str::FromStr; use std::time::Duration; -use anyhow::anyhow; use crate::identity::credentials::app_config::AppConfig; -use url::Url; - -/* -fn response_to_http_response(response: reqwest::Response) -> anyhow::Result<http::Response<>> { - let status = response.status(); - let url = response.url().clone(); - let headers = response.headers().clone(); - let version = response.version(); - - let body: serde_json::Value = response.json().await?; - let next_link = body.odata_next_link(); - let json = body.clone(); - let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) - .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); - - let mut builder = http::Response::builder() - .url(url) - .json(&json) - .status(http::StatusCode::from(&status)) - .version(version); - - for builder_header in builder.headers_mut().iter_mut() { - builder_header.extend(headers.clone()); - } - Ok(builder.body(body_result)) -} - */ +use url::Url; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -94,80 +72,100 @@ impl DeviceCodeCredential { self } -/* - pub async fn poll_async(&mut self, buffer: Option<usize>) -> tokio::sync::mpsc::Receiver<anyhow::Result<http::Response<serde_json::Value>>> { + pub async fn poll_async( + &mut self, + buffer: Option<usize>, + ) -> AuthExecutionResult<tokio::sync::mpsc::Receiver<http::Response<serde_json::Value>>> { let (sender, receiver) = { if let Some(buffer) = buffer { tokio::sync::mpsc::channel(buffer) - } else { + } else { tokio::sync::mpsc::channel(100) } }; let mut credential = self.clone(); - let mut application = PublicClientApplication::from(self.clone()); - - tokio::spawn(async move { - let response = application.execute_async().await.map_err(|err| anyhow!(err)); - - match response { - Ok(response) => { - let status = response.status(); - - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); - - let device_code = body["device_code"].as_str().unwrap(); - let interval = body["interval"].as_u64().unwrap(); - let message = body["message"].as_str().unwrap(); - credential.with_device_code(device_code); - let mut application = PublicClientApplication::from(credential); - - if !status.is_success() { - loop { - // Wait the amount of seconds that interval is. - std::thread::sleep(Duration::from_secs(interval.clone())); - - let response = application.execute_async().await.unwrap(); - - let status = response.status(); - println!("{response:#?}"); - - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); + let result = credential + .execute_async() + .await + .map_err(AuthExecutionError::from); + + match result { + Ok(response) => { + let device_code_response: DeviceCode = response.json().await.unwrap(); + println!("{:#?}", device_code_response); + + let device_code = device_code_response.device_code; + let interval = device_code_response.interval; + let _message = device_code_response.message; + credential.with_device_code(device_code); + + tokio::spawn(async move { + let mut should_slow_down = false; + + loop { + // Wait the amount of seconds that interval is. + if should_slow_down { + std::thread::sleep(interval.add(Duration::from_secs(5))); + } else { + std::thread::sleep(interval); + } - if status.is_success() { - sender.send_timeout(Ok(body), Duration::from_secs(60)); - } else { - let option_error = body["error"].as_str(); - - if let Some(error) = option_error { - match error { - "authorization_pending" => println!("Still waiting on user to sign in"), - "authorization_declined" => panic!("user declined to sign in"), - "bad_verification_code" => println!("Bad verification code. Message:\n{message:#?}"), - "expired_token" => panic!("token has expired - user did not sign in"), - _ => { - panic!("This isn't the error we expected: {error:#?}"); + let response = credential.execute_async().await.unwrap(); + + let status = response.status(); + println!("{response:#?}"); + + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); + + if status.is_success() { + sender + .send_timeout( + http::Response::builder().status(status).body(body).unwrap(), + Duration::from_secs(60), + ) + .await + .unwrap(); + } else { + let option_error = body["error"].as_str().map(|value| value.to_owned()); + sender + .send_timeout( + http::Response::builder().status(status).body(body).unwrap(), + Duration::from_secs(60), + ) + .await + .unwrap(); + + if let Some(error) = option_error { + match PollDeviceCodeType::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeType::AuthorizationPending => continue, + PollDeviceCodeType::AuthorizationDeclined => break, + PollDeviceCodeType::BadVerificationCode => continue, + PollDeviceCodeType::ExpiredToken => break, + PollDeviceCodeType::InvalidType => break, + PollDeviceCodeType::AccessDenied => break, + PollDeviceCodeType::SlowDown => { + should_slow_down = true; + continue; } - } - } else { - // Body should have error or we should bail. - panic!("Crap hit the fan"); + }, + Err(_) => break, } + } else { + // Body should have error or we should bail. + break; } } } - } - Err(err) => { - sender.send_timeout(Err(err), Duration::from_secs(60)).await; - } + }); } - }); + Err(err) => return Err(err), + } - return receiver; + Ok(receiver) } - */ pub fn builder() -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new() @@ -175,22 +173,22 @@ impl DeviceCodeCredential { } impl TokenCredentialExecutor for DeviceCodeCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.app_config.authority); + .authority_device_code(azure_cloud_instance, &self.app_config.authority); - if self.refresh_token.is_none() { + if self.device_code.is_none() && self.refresh_token.is_none() { let uri = self .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("access_token_url"))?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + .get(OAuthParameter::AuthorizationUrl) + .ok_or(AF::msg_internal_err("authorization_url"))?; + Url::parse(uri.as_str()).map_err(|_err| AF::msg_internal_err("authorization_url")) } else { let uri = self .serializer - .get(OAuthParameter::RefreshTokenUrl) - .ok_or(AF::msg_internal_err("refresh_token_url"))?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_internal_err("token_url"))?; + Url::parse(uri.as_str()).map_err(|_err| AF::msg_internal_err("token_url")) } } @@ -248,15 +246,9 @@ impl TokenCredentialExecutor for DeviceCodeCredential { ); } - self.serializer.grant_type(DEVICE_CODE_GRANT_TYPE); - self.serializer.as_credential_map( vec![], - vec![ - OAuthParameter::ClientId, - OAuthParameter::Scope, - OAuthParameter::GrantType, - ], + vec![OAuthParameter::ClientId, OAuthParameter::Scope], ) } @@ -291,6 +283,33 @@ impl DeviceCodeCredentialBuilder { } } + pub(crate) fn new_with_app_config(app_config: AppConfig) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder { + credential: DeviceCodeCredential { + app_config, + refresh_token: None, + device_code: None, + scope: vec![], + serializer: Default::default(), + }, + } + } + + pub(crate) fn new_with_device_code<T: AsRef<str>>( + device_code: T, + app_config: AppConfig, + ) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder { + credential: DeviceCodeCredential { + app_config, + refresh_token: None, + device_code: Some(device_code.as_ref().to_owned()), + scope: vec![], + serializer: Default::default(), + }, + } + } + pub fn with_device_code<T: AsRef<str>>(&mut self, device_code: T) -> &mut Self { self.credential.device_code = Some(device_code.as_ref().to_owned()); self.credential.refresh_token = None; diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 22dbca1c..d4d84428 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -25,7 +25,6 @@ mod open_id_credential; mod prompt; mod proof_key_for_code_exchange; mod public_client_application; -mod public_client_application_builder; mod resource_owner_password_credential; mod response_mode; mod response_type; @@ -56,7 +55,6 @@ pub use open_id_credential::*; pub use prompt::*; pub use proof_key_for_code_exchange::*; pub use public_client_application::*; -pub use public_client_application_builder::*; pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 47a4a5db..60830e80 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -1,14 +1,14 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType, }; use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; -use std::collections::BTreeSet; use reqwest::IntoUrl; +use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; -use crate::identity::credentials::app_config::AppConfig; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your @@ -160,14 +160,8 @@ impl OpenIdAuthorizationUrl { } impl AuthorizationUrl for OpenIdAuthorizationUrl { - fn redirect_uri(&self) -> AuthorizationResult<Url> { - let redirect_uri = self.app_config.redirect_uri.as_ref() - .ok_or(AuthorizationFailure::msg_err( - "redirect_uri", - "If not provided, the authorization server will pick one registered redirect_uri at random to send the user back to" - ))?; - - Url::parse(redirect_uri.as_str()).map_err(AuthorizationFailure::from) + fn redirect_uri(&self) -> Option<&Url> { + self.app_config.redirect_uri.as_ref() } fn authorization_url(&self) -> AuthorizationResult<Url> { @@ -250,6 +244,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { let mut encoder = Serializer::new(String::new()); serializer.encode_query( vec![ + OAuthParameter::ResponseMode, OAuthParameter::RedirectUri, OAuthParameter::State, OAuthParameter::Prompt, @@ -257,7 +252,6 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { OAuthParameter::DomainHint, ], vec![ - OAuthParameter::ResponseMode, OAuthParameter::ClientId, OAuthParameter::ResponseType, OAuthParameter::Scope, @@ -320,7 +314,8 @@ impl OpenIdAuthorizationUrlBuilder { /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.auth_url_parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.auth_url_parameters.app_config.authority = + Authority::TenantId(tenant.as_ref().to_owned()); self } @@ -481,7 +476,8 @@ mod test { #[test] #[should_panic] fn unsupported_response_type() { - let _ = OpenIdAuthorizationUrl::builder().unwrap() + let _ = OpenIdAuthorizationUrl::builder() + .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) .with_client_id("client_id") .with_scope(["scope"]) diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 195e0fbb..68101605 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,10 +1,11 @@ use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::application_builder::PublicClientApplicationBuilder; use crate::identity::{ Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredentialExecutor, }; use async_trait::async_trait; -use graph_error::AuthorizationResult; +use graph_error::{AuthExecutionResult, AuthorizationResult}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; @@ -42,12 +43,16 @@ impl PublicClientApplication { credential: Box::new(credential), } } + + pub fn builder(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { + PublicClientApplicationBuilder::new(client_id.as_ref()) + } } #[async_trait] impl TokenCredentialExecutor for PublicClientApplication { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_authority_host) + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.credential.uri(azure_cloud_instance) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -70,7 +75,7 @@ impl TokenCredentialExecutor for PublicClientApplication { self.credential.app_config() } - fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let azure_authority_host = self.azure_cloud_instance(); let uri = self.credential.uri(&azure_authority_host)?; @@ -98,7 +103,7 @@ impl TokenCredentialExecutor for PublicClientApplication { } } - async fn execute_async(&mut self) -> anyhow::Result<Response> { + async fn execute_async(&mut self) -> AuthExecutionResult<Response> { let azure_cloud_instance = self.credential.azure_cloud_instance(); let uri = self.credential.uri(&azure_cloud_instance)?; diff --git a/graph-oauth/src/identity/credentials/public_client_application_builder.rs b/graph-oauth/src/identity/credentials/public_client_application_builder.rs deleted file mode 100644 index 39e58d88..00000000 --- a/graph-oauth/src/identity/credentials/public_client_application_builder.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::identity::{EnvironmentCredential, PublicClientApplication}; -use std::env::VarError; - -pub struct PublicClientApplicationBuilder; - -impl PublicClientApplicationBuilder { - pub fn try_from_environment() -> Result<PublicClientApplication, VarError> { - EnvironmentCredential::resource_owner_password_credential() - } -} diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 7cfc9ebb..96be9876 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -69,7 +69,7 @@ impl ResourceOwnerPasswordCredential { impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(&azure_authority_host, &self.app_config.authority); + .authority(azure_authority_host, &self.app_config.authority); let uri = self .serializer diff --git a/test_files/application_options/aad_options.json b/graph-oauth/src/identity/credentials/test/application_options/aad_options.json similarity index 100% rename from test_files/application_options/aad_options.json rename to graph-oauth/src/identity/credentials/test/application_options/aad_options.json diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 6440c65c..eb5c0fd4 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -2,7 +2,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance}; use async_trait::async_trait; -use graph_error::AuthorizationResult; +use graph_error::{AuthExecutionResult, AuthorizationResult}; use http::header::ACCEPT; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; @@ -10,35 +10,6 @@ use reqwest::ClientBuilder; use std::collections::HashMap; use url::Url; -/* -fn http_response(response: reqwest::blocking::Response) { - let status = response.status(); - let url = response.url().clone(); - let headers = response.headers().clone(); - let version = response.version(); - - let mut builder = http::Response::builder() - .url(url) - .status(http::StatusCode::from(&status)) - .version(version); - - for builder_header in builder.headers_mut().iter_mut() { - builder_header.extend(headers.clone()); - } - - let body_result: reqwest::Result<serde_json::Value> = response.json(); -// MsalTokenResponse - if let Ok(body) = body_result.as_ref() { - let token: serde_json::Result<MsalTokenResponse> = serde_json::from_value(body.clone()); - builder.json(body.clone()); - builder.body(token) - } else { - - } -} - - */ - #[async_trait] pub trait TokenCredentialExecutor { fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; @@ -67,7 +38,7 @@ pub trait TokenCredentialExecutor { )?) } - fn get_openid_config(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + fn get_openid_config(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let open_id_url = self.openid_configuration_url()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -85,7 +56,7 @@ pub trait TokenCredentialExecutor { Ok(response) } - async fn get_openid_config_async(&mut self) -> anyhow::Result<reqwest::Response> { + async fn get_openid_config_async(&mut self) -> AuthExecutionResult<reqwest::Response> { let open_id_config_url = self.openid_configuration_url()?; let http_client = ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -105,7 +76,7 @@ pub trait TokenCredentialExecutor { Ok(response) } - fn execute(&mut self) -> anyhow::Result<reqwest::blocking::Response> { + fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let options = self.azure_cloud_instance(); let uri = self.uri(&options)?; let form = self.form_urlencode()?; @@ -132,7 +103,7 @@ pub trait TokenCredentialExecutor { } } - async fn execute_async(&mut self) -> anyhow::Result<reqwest::Response> { + async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { let azure_cloud_instance = self.azure_cloud_instance(); let uri = self.uri(&azure_cloud_instance)?; let form = self.form_urlencode()?; diff --git a/graph-oauth/src/identity/device_code.rs b/graph-oauth/src/identity/device_code.rs new file mode 100644 index 00000000..2eee6f92 --- /dev/null +++ b/graph-oauth/src/identity/device_code.rs @@ -0,0 +1,105 @@ +use serde_json::Value; +use std::collections::{BTreeSet, HashMap}; +use std::str::FromStr; +use std::time::Duration; + +/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 +/// The actual device code response that is received from Microsoft Graph +/// looks similar to the following: +/// +/// ```json +/// { +/// "device_code": String("FABABAAEAAAD--DLA3VO7QrddgJg7WevrgJ7Czy_TDsDClt2ELoEC8ePWFs"), +/// "expires_in": Number(900), +/// "interval": Number(5), +/// "message": String("To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code FQK5HW3UF to authenticate."), +/// "user_code": String("FQK5HW3UF"), +/// "verification_uri": String("https://microsoft.com/devicelogin"), +/// } +/// ``` +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct DeviceCode { + /// A long string used to verify the session between the client and the authorization server. + /// The client uses this parameter to request the access token from the authorization server. + pub device_code: String, + /// The number of seconds before the device_code and user_code expire. + pub expires_in: u64, + /// OPTIONAL + /// The minimum amount of time in seconds that the client + /// SHOULD wait between polling requests to the token endpoint. If no + /// value is provided, clients MUST use 5 as the default. + #[serde(default = "default_interval")] + pub interval: Duration, + /// User friendly text response that can be used for display purpose. + pub message: String, + pub user_code: String, + /// Verification URL where the user must navigate to authenticate using the device code + /// and credentials. + pub verification_uri: String, + /// The verification_uri_complete response field is not included or supported + /// by Microsoft at this time. + pub verification_uri_complete: Option<String>, + /// List of the scopes that would be held by token. + pub scopes: Option<BTreeSet<String>>, + pub error: Option<String>, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, +} + +fn default_interval() -> Duration { + Duration::from_secs(5) +} + +/// Response types used when polling for a device code +/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum PollDeviceCodeType { + /// The user hasn't finished authenticating, but hasn't canceled the flow. + /// Repeat the request after at least interval seconds. + AuthorizationPending, + /// The end user denied the authorization request. + /// Stop polling and revert to an unauthenticated state. + AuthorizationDeclined, + /// The device_code sent to the /token endpoint wasn't recognized. + /// Verify that the client is sending the correct device_code in the request. + BadVerificationCode, + /// Value of expires_in has been exceeded and authentication is no longer possible + /// with device_code. + /// Stop polling and revert to an unauthenticated state. + ExpiredToken, + + /// Not yet supported by Microsoft but listed in the specification. + /// + /// The authorization request was denied. + AccessDenied, + + /// Not yet supported by Microsoft but listed in the specification. + /// + /// A variant of "authorization_pending", the authorization request is + /// still pending and polling should continue, but the interval MUST + /// be increased by 5 seconds for this and all subsequent requests. + SlowDown, + + /// Indicates the value is not an actual PollDeviceCodeType - this is an internal type not a + /// type used in Microsoft Identity Platform or in the OAuth2/OpenId specification. + /// + /// This is a catch all to prevent parsing errors and break from + /// any loop that is used to poll for the device code. + InvalidType, +} + +impl FromStr for PollDeviceCodeType { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "authorization_pending" => Ok(PollDeviceCodeType::AuthorizationPending), + "authorization_declined" => Ok(PollDeviceCodeType::AuthorizationDeclined), + "bad_verification_code" => Ok(PollDeviceCodeType::BadVerificationCode), + "expired_token" => Ok(PollDeviceCodeType::ExpiredToken), + "access_denied" => Ok(PollDeviceCodeType::AccessDenied), + "slow_down" => Ok(PollDeviceCodeType::SlowDown), + _ => Ok(PollDeviceCodeType::InvalidType), + } + } +} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index e974c0c1..d5124589 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,15 +1,19 @@ mod allowed_host_validator; mod application_options; mod authority; +mod authorization_query_response; mod authorization_serializer; mod credential_store; mod credentials; +mod device_code; pub use allowed_host_validator::*; pub use authority::*; +pub use authorization_query_response::*; pub use authorization_serializer::*; pub use credential_store::*; pub use credentials::*; +pub use device_code::*; #[cfg(feature = "openssl")] pub use openssl::{ diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 8f696771..8de4e4f1 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -28,28 +28,26 @@ //! //! //! # Example -//! ``` -//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication, CredentialBuilder}; +//! ```rust +//! use url::Url; +//! use graph_error::AuthorizationResult; +//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! -//! pub fn authorization_url(client_id: &str) { -//! let _url = AuthorizationCodeCredential::authorization_url_builder() +//! pub fn authorization_url(client_id: &str) -> AuthorizationResult<Url> { +//! AuthorizationCodeCredential::authorization_url_builder() //! .with_client_id(client_id) //! .with_redirect_uri("http://localhost:8000/redirect") //! .with_scope(vec!["user.read"]) //! .url() -//! .unwrap(); //! } //! //! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication> { -//! let credential = AuthorizationCodeCredential::builder() +//! Ok(AuthorizationCodeCredential::builder(client_id) //! .with_authorization_code(authorization_code) -//! .with_client_id(client_id) //! .with_client_secret(client_secret) //! .with_scope(vec!["user.read"]) //! .with_redirect_uri("http://localhost:8000/redirect")? -//! .build(); -//! -//! Ok(ConfidentialClientApplication::from(credential)) +//! .build()) //! } //! ``` @@ -60,15 +58,13 @@ extern crate serde; #[macro_use] extern crate log; extern crate pretty_env_logger; + mod access_token; mod auth; -mod auth_response_query; -mod device_code; mod discovery; mod grants; mod id_token; pub mod jwt; -mod oauth2_header; mod oauth_error; pub mod identity; @@ -79,8 +75,6 @@ pub mod oauth { pub use crate::auth::GrantSelector; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; - pub use crate::auth_response_query::*; - pub use crate::device_code::*; pub use crate::discovery::graph_discovery; pub use crate::discovery::jwt_keys; pub use crate::discovery::well_known; @@ -88,7 +82,6 @@ pub mod oauth { pub use crate::grants::GrantType; pub use crate::id_token::IdToken; pub use crate::identity::*; - pub use crate::oauth2_header::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; } diff --git a/graph-oauth/src/oauth2_header.rs b/graph-oauth/src/oauth2_header.rs deleted file mode 100644 index 8b137891..00000000 --- a/graph-oauth/src/oauth2_header.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/lib.rs b/src/lib.rs index 4a0db384..6d8465c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -247,11 +247,11 @@ pub mod oauth { } pub mod http { + pub use graph_extensions::http::{HttpResponseBuilderExt, HttpResponseExt}; pub use graph_http::api_impl::{BodyRead, FileConfig, UploadSession}; pub use graph_http::traits::{ - AsyncIterator, HttpResponseBuilderExt, HttpResponseExt, ODataDeltaLink, ODataDownloadLink, - ODataMetadataLink, ODataNextLink, ODataQuery, ResponseBlockingExt, ResponseExt, - UploadSessionLink, + AsyncIterator, ODataDeltaLink, ODataDownloadLink, ODataMetadataLink, ODataNextLink, + ODataQuery, ResponseBlockingExt, ResponseExt, UploadSessionLink, }; pub use reqwest::tls::Version; pub use reqwest::{Body, Method}; From ee423503deb1a2a4830f302ece85d9da3f960f50 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 5 Sep 2023 04:40:25 -0400 Subject: [PATCH 036/118] Use Uuid's for client id types --- Cargo.toml | 1 + examples/oauth/README.md | 14 ++-- .../auth_code_grant/auth_code_grant_secret.rs | 21 +++-- examples/oauth/device_code.rs | 39 ++++++--- examples/oauth/openid_connect/mod.rs | 4 +- ..._server_auth.rs => openid_local_server.rs} | 25 +++--- examples/oauth_authorization_url/README.md | 52 ++++++++++++ .../{ => legacy}/implicit_grant.rs | 0 .../oauth_authorization_url/legacy/mod.rs | 1 + examples/oauth_authorization_url/main.rs | 6 +- .../open_id_connect.rs | 12 --- .../oauth_authorization_url/openid_connect.rs | 28 +++++++ examples/oauth_certificate/main.rs | 6 +- graph-error/Cargo.toml | 1 + graph-error/src/authorization_failure.rs | 4 +- graph-http/src/lib.rs | 2 +- graph-oauth/Cargo.toml | 4 +- .../src/identity/application_options.rs | 6 +- .../src/identity/credentials/app_config.rs | 20 ++--- .../credentials/application_builder.rs | 25 +++--- .../auth_code_authorization_url_parameters.rs | 26 +++--- ...thorization_code_certificate_credential.rs | 17 ++-- .../authorization_code_credential.rs | 26 +++--- .../client_assertion_credential.rs | 9 ++- .../credentials/client_builder_impl.rs | 10 +-- .../client_certificate_credential.rs | 80 ++++++++++++------- .../credentials/client_secret_credential.rs | 20 +++-- .../confidential_client_application.rs | 3 +- .../credentials/device_code_credential.rs | 25 +++--- .../credentials/environment_credential.rs | 7 +- .../credentials/implicit_credential.rs | 9 ++- .../credentials/open_id_authorization_url.rs | 14 ++-- .../credentials/open_id_credential.rs | 19 +++-- .../credentials/public_client_application.rs | 3 +- .../resource_owner_password_credential.rs | 57 +++++-------- .../src/identity/credentials/response_type.rs | 11 --- .../test/application_options/aad_options.json | 2 +- .../credentials/token_credential_executor.rs | 10 +-- .../credentials/token_credential_options.rs | 2 +- graph-oauth/src/identity/mod.rs | 1 + test-tools/src/oauth_request.rs | 2 +- tests/todo_tasks_request.rs | 40 ++++++++++ 42 files changed, 410 insertions(+), 254 deletions(-) rename examples/oauth/openid_connect/{openid_local_and_server_auth.rs => openid_local_server.rs} (87%) create mode 100644 examples/oauth_authorization_url/README.md rename examples/oauth_authorization_url/{ => legacy}/implicit_grant.rs (100%) create mode 100644 examples/oauth_authorization_url/legacy/mod.rs delete mode 100644 examples/oauth_authorization_url/open_id_connect.rs create mode 100644 examples/oauth_authorization_url/openid_connect.rs create mode 100644 tests/todo_tasks_request.rs diff --git a/Cargo.toml b/Cargo.toml index 8deeb5ad..0769a301 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ openssl = ["graph-oauth/openssl"] [dev-dependencies] bytes = { version = "1.4.0" } futures = "0.3" +http = "0.2.9" lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } warp = { version = "0.3.5" } diff --git a/examples/oauth/README.md b/examples/oauth/README.md index f62e5653..c2fac63a 100644 --- a/examples/oauth/README.md +++ b/examples/oauth/README.md @@ -6,21 +6,21 @@ Getting the Confidential Client ```rust use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, + ConfidentialClientApplication, }; fn main() { let authorization_code = "<AUTH_CODE>"; let client_id = "<CLIENT_ID>"; let client_secret = "<CLIENT_SECRET>"; + let scope = vec!["<SCOPE>", "<SCOPE>"]; - let auth_code_credential = AuthorizationCodeCredential::builder(authorization_code) - .with_client_id(client_id) + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_authorization_code(authorization_code) .with_client_secret(client_secret) - .with_scope(vec!["files.read", "offline_access"]) - .with_redirect_uri("http://localhost:8000/redirect")? + .with_scope(SCOPE.clone()) + .with_redirect_uri(REDIRECT_URI) + .unwrap() .build(); - - let confidential_client = ConfidentialClientApplication::from(auth_code_credential); } ``` diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index dfc856df..1fcad3e7 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -1,3 +1,4 @@ +use graph_rs_sdk::error::ErrorMessage; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, MsalTokenResponse, TokenCredentialExecutor, TokenRequest, @@ -5,8 +6,12 @@ use graph_rs_sdk::oauth::{ use graph_rs_sdk::*; use warp::Filter; +// Update these values with your own or provide them directly in the +// methods below. static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static SCOPE: &str = "User.Read"; #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct AccessCode { @@ -16,8 +21,8 @@ pub struct AccessCode { pub fn authorization_sign_in() { let url = AuthorizationCodeCredential::authorization_url_builder() .with_client_id(CLIENT_ID) - .with_redirect_uri("http://localhost:8000/redirect") - .with_scope(vec!["offline_access", "files.read"]) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -62,11 +67,11 @@ async fn handle_redirect( // Set the access code and request an access token. // Callers should handle the Result from requesting an access token // in case of an error here. - let mut confidential_client = AuthorizationCodeCredential::builder(authorization_code) - .with_client_id(CLIENT_ID) + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_authorization_code(authorization_code) .with_client_secret(CLIENT_SECRET) - .with_scope(vec!["files.read", "offline_access"]) - .with_redirect_uri("http://localhost:8000/redirect") + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) .unwrap() .build(); @@ -83,10 +88,10 @@ async fn handle_redirect( // This will print the actual access token to the console. } else { // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; + let result: reqwest::Result<ErrorMessage> = response.json().await; match result { - Ok(body) => println!("{body:#?}"), + Ok(error_message) => println!("{error_message:#?}"), Err(err) => println!("Error on deserialization:\n{err:#?}"), } } diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index a714a958..8aa6d77e 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,23 +1,36 @@ -use graph_oauth::identity::{DeviceCodeCredential, TokenCredentialExecutor}; +use graph_oauth::identity::{ + DeviceCodeCredential, PublicClientApplication, TokenCredentialExecutor, +}; use graph_oauth::oauth::DeviceCodeCredentialBuilder; use graph_rs_sdk::oauth::{MsalTokenResponse, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; +use warp::hyper::body::HttpBody; // https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code -// Update the client id with your own. -fn get_oauth() -> OAuthSerializer { - let client_id = "CLIENT_ID"; - let mut oauth = OAuthSerializer::new(); +static CLIENT_ID: &str = "<CLIENT_ID>"; +static TENANT: &str = "<TENANT>"; - oauth - .client_id(client_id) - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/devicecode") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .add_scope("files.read") - .add_scope("offline_access"); +// Make the call to get a device code from the user. +fn get_auth_call_for_device_code() { + let mut public_client = PublicClientApplication::builder(CLIENT_ID) + .with_device_code_builder() + .with_scope(["User.Read"]) + .with_tenant(TENANT); +} + +fn get_token(device_code: &str) { + let mut public_client = PublicClientApplication::builder(CLIENT_ID) + .with_device_code(device_code) + .with_scope(["User.Read"]) + .with_tenant(TENANT) + .build(); + + let response = public_client.execute().unwrap(); + println!("{:#?}", response); + + let body: MsalTokenResponse = response.json().unwrap(); - oauth + println!("{:#?}", body); } diff --git a/examples/oauth/openid_connect/mod.rs b/examples/oauth/openid_connect/mod.rs index cb19e577..bc61d951 100644 --- a/examples/oauth/openid_connect/mod.rs +++ b/examples/oauth/openid_connect/mod.rs @@ -1,3 +1,3 @@ -mod openid_local_and_server_auth; +mod openid_local_server; -pub use openid_local_and_server_auth::*; +pub use openid_local_server::*; diff --git a/examples/oauth/openid_connect/openid_local_and_server_auth.rs b/examples/oauth/openid_connect/openid_local_server.rs similarity index 87% rename from examples/oauth/openid_connect/openid_local_and_server_auth.rs rename to examples/oauth/openid_connect/openid_local_server.rs index 0e12f0f4..d4bf9120 100644 --- a/examples/oauth/openid_connect/openid_local_and_server_auth.rs +++ b/examples/oauth/openid_connect/openid_local_server.rs @@ -32,17 +32,17 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; fn openid_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { Ok(OpenIdCredential::authorization_url_builder()? - .with_client_id(CLIENT_ID) - .with_tenant(TENANT_ID) - //.with_default_scope()? - .with_redirect_uri("http://localhost:8000/redirect")? - .with_response_mode(ResponseMode::FormPost) - .with_response_type([ResponseType::IdToken, ResponseType::Code]) - .with_prompt(Prompt::SelectAccount) - .with_state(REDIRECT_URI) - .extend_scope(vec!["User.Read", "User.ReadWrite"]) - .build() - .url()?) + .with_client_id(CLIENT_ID) + .with_tenant(TENANT_ID) + //.with_default_scope()? + .with_redirect_uri("http://localhost:8000/redirect")? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state(REDIRECT_URI) + .extend_scope(vec!["User.Read", "User.ReadWrite"]) + .build() + .url()?) } async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, warp::Rejection> { @@ -54,7 +54,8 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) .with_openid(code, CLIENT_SECRET) .with_tenant(TENANT_ID) - .with_redirect_uri(REDIRECT_URI).unwrap() + .with_redirect_uri(REDIRECT_URI) + .unwrap() .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope .build(); diff --git a/examples/oauth_authorization_url/README.md b/examples/oauth_authorization_url/README.md new file mode 100644 index 00000000..ed0701cf --- /dev/null +++ b/examples/oauth_authorization_url/README.md @@ -0,0 +1,52 @@ +# Building an Authorization URL + +The authorization request is the initial request to sign in where the user +is taken to the sign in page and enters their credentials. + +If successful the user will be redirected back to your app and the authorization +code will be in the query of the URL. + +## Examples + + +### OpenId Connect + +#### Required Parameters (see [Send the sign-in request](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-the-sign-in-request) in the Microsoft Identity Platform Documentation): + +The tenant, response type, scope, and nonce parameters all have default values that are automatically set by +the client. The scope parameter can and should include more than just the default value (openid). + +* **Tenant** + * Required. Defaults to `common` when not provided to the client and is automatically set by the client. + * Definition: You can use the {tenant} value in the path of the request to control who can sign in to the application. + The allowed values are common, organizations, consumers, and tenant identifiers. +* **Client Id** - + * Required. + * Definition: The Application (client) ID that the Azure portal – App registrations experience assigned to your app. +* **Response Type** + * Required. Defaults to `id_token` in the client and is automatically set by the client. + * Definition: Must include `id_token` for OpenID Connect sign-in. +* **Scope** + * Required. The scope `openid` is automatically set by the client. + * Definition: A space-separated list of scopes. For OpenID Connect, it must include the scope openid, which translates to the + Sign you in permission in the consent UI. You might also include other scopes in this request for requesting consent. +* **Nonce** + * Required. The client generates a nonce using the same secure cryptographic algorithm that is used for PKCE flows. You can also provide your own. + * Definition: A value generated and sent by your app in its request for an ID token. + The same nonce value is included in the ID token returned to your app by the Microsoft identity platform. + To mitigate token replay attacks, your app should verify the nonce value in the ID token is the same value it sent + when requesting the token. The value is typically a unique, random string. + +```rust +use graph_oauth::identity::OpenIdCredential; + +fn open_id_authorization_url(client_id: &str, tenant: &str, redirect_uri: &str, scope: Vec<&str>) -> anyhow::Result<Url> { + Ok(OpenIdCredential::authorization_url_builder()? + .with_client_id(client_id) + .with_tenant(tenant) + .with_redirect_uri(redirect_uri)? + .extend_scope(scope) + .build() + .url()?) +} +``` diff --git a/examples/oauth_authorization_url/implicit_grant.rs b/examples/oauth_authorization_url/legacy/implicit_grant.rs similarity index 100% rename from examples/oauth_authorization_url/implicit_grant.rs rename to examples/oauth_authorization_url/legacy/implicit_grant.rs diff --git a/examples/oauth_authorization_url/legacy/mod.rs b/examples/oauth_authorization_url/legacy/mod.rs new file mode 100644 index 00000000..6a511aa2 --- /dev/null +++ b/examples/oauth_authorization_url/legacy/mod.rs @@ -0,0 +1 @@ +mod implicit_grant; diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index c1fafa6c..46e40f5b 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -8,8 +8,8 @@ #[macro_use] extern crate serde; -mod implicit_grant; -mod open_id_connect; +mod legacy; +mod openid_connect; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, @@ -28,7 +28,7 @@ pub fn auth_code_grant_authorization() { let url = AuthorizationCodeCredential::authorization_url_builder() .with_client_id(CLIENT_ID) .with_redirect_uri("http://localhost:8000/redirect") - .with_scope(vec!["offline_access", "files.read"]) + .with_scope(vec!["user.read"]) .url() .unwrap(); diff --git a/examples/oauth_authorization_url/open_id_connect.rs b/examples/oauth_authorization_url/open_id_connect.rs deleted file mode 100644 index 4f584be4..00000000 --- a/examples/oauth_authorization_url/open_id_connect.rs +++ /dev/null @@ -1,12 +0,0 @@ -use graph_oauth::oauth::OpenIdCredential; -use url::Url; - -// Use your client id and client secret found in the Azure Portal -fn open_id_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { - Ok(OpenIdCredential::authorization_url_builder()? - .with_client_id(client_id) - .with_default_scope()? - .extend_scope(vec!["Files.Read"]) - .build() - .url()?) -} diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs new file mode 100644 index 00000000..87503308 --- /dev/null +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -0,0 +1,28 @@ +use graph_oauth::identity::OpenIdCredential; +use url::Url; + +// The authorization request is the initial request to sign in where the user +// is taken to the sign in page and enters their credentials. +// If successful the user will be redirected back to your app and the authorization +// code will be in the query of the URL. + +// The URL builder below will create the full URL with the query that you will +// need to send the user to such as redirecting the page they are on when using +// your app to the URL. + +// See examples/oauth/openid_connect for a full example. + +fn open_id_authorization_url( + client_id: &str, + tenant: &str, + redirect_uri: &str, + scope: Vec<&str>, +) -> anyhow::Result<Url> { + Ok(OpenIdCredential::authorization_url_builder()? + .with_client_id(client_id) + .with_tenant(tenant) + .with_redirect_uri(redirect_uri)? + .extend_scope(scope) + .build() + .url()?) +} diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 67e3d132..9f5ae731 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -87,9 +87,9 @@ pub fn x509_certificate(client_id: &str, tenant_id: &str) -> anyhow::Result<X509 } // When the authorization code comes in on the redirect from sign in, call the get_credential -// method passing in the authorization code. The AuthorizationCodeCertificateCredential can be passed -// to a confidential client application in order to exchange the authorization code -// for an access token. +// method passing in the authorization code. +// Building AuthorizationCodeCertificateCredential will create a ConfidentialClientApplication +// which can be used to exchange the authorization code for an access token. async fn handle_redirect( code_option: Option<AccessCode>, ) -> Result<Box<dyn warp::Reply>, warp::Rejection> { diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 5a2edceb..6b3b55db 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -25,3 +25,4 @@ thiserror = "1" tokio = { version = "1.25.0", features = ["full"] } url = "2" x509-parser = "0.15.0" +uuid = { version = "1.3.1" } diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index bfa9d1da..4e0c1bd1 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,6 +1,5 @@ use crate::AuthorizationResult; - pub type AF = AuthorizationFailure; #[derive(Debug, thiserror::Error)] @@ -14,6 +13,9 @@ pub enum AuthorizationFailure { #[error("{0:#?}")] UrlParseError(#[from] url::ParseError), + #[error("{0:#?}")] + UuidError(#[from] uuid::Error), + #[error("{0:#?}")] Unknown(String), } diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 6f49d993..051cb0f4 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -36,7 +36,7 @@ pub mod api_impl { pub use crate::client::*; pub use crate::core::*; pub use crate::request_components::RequestComponents; - pub use crate::request_handler::RequestHandler; + pub use crate::request_handler::{PagingResponse, PagingResult, RequestHandler}; pub use crate::resource_identifier::{ResourceConfig, ResourceIdentifier}; pub use crate::traits::{ApiClientImpl, BodyExt, ODataQuery}; pub use crate::upload_session::UploadSession; diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index ba20c8a9..47785cea 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -34,8 +34,8 @@ strum = { version = "0.24.1", features = ["derive"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset"] } webbrowser = "0.8.7" -wry = "0.28.3" -uuid = { version = "1.3.1", features = ["v4"] } +wry = "0.30.0" +uuid = { version = "1.3.1", features = ["v4", "serde"] } log = "0.4" pretty_env_logger = "0.4" tokio = { version = "1.27.0", features = ["full"] } diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 1e9b5ca0..7aaf5e1c 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -1,6 +1,7 @@ use crate::identity::AadAuthorityAudience; use crate::oauth::AzureCloudInstance; use url::Url; +use uuid::Uuid; /// Application Options typically stored as JSON file in .net applications. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] @@ -9,7 +10,7 @@ pub struct ApplicationOptions { /// application registration portal (https://aka.ms/msal-net-register-app) /// Required parameter for ApplicationOptions. #[serde(alias = "clientId", alias = "ClientId", alias = "client_id")] - pub client_id: String, + pub client_id: Uuid, /// Tenant from which the application will allow users to sign it. This can be: /// a domain associated with a tenant, a GUID (tenant id), or a meta-tenant (e.g. consumers). /// This property is mutually exclusive with [AadAuthorityAudience]. If both @@ -37,7 +38,8 @@ pub struct ApplicationOptions { impl ApplicationOptions { pub fn new(client_id: impl AsRef<str>) -> ApplicationOptions { ApplicationOptions { - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref()) + .expect("Invalid Client Id - Must be a valid Uuid"), tenant_id: None, aad_authority_audience: None, instance: None, diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 86c8a244..eab8a5e4 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,8 +1,8 @@ -use crate::identity::credentials::application_builder::AuthorityHost; -use crate::identity::Authority; +use crate::identity::{Authority, AzureCloudInstance}; use reqwest::header::HeaderMap; use std::collections::HashMap; use url::Url; +use uuid::Uuid; #[derive(Clone, Debug, Default, PartialEq)] pub struct AppConfig { @@ -14,9 +14,9 @@ pub struct AppConfig { /// Required. /// The Application (client) ID that the Azure portal - App registrations page assigned /// to your app - pub(crate) client_id: String, + pub(crate) client_id: Uuid, pub(crate) authority: Authority, - pub(crate) authority_url: AuthorityHost, + pub(crate) azure_cloud_instance: AzureCloudInstance, pub(crate) extra_query_parameters: HashMap<String, String>, pub(crate) extra_header_parameters: HeaderMap, /// Optional - Some flows may require the redirect URI @@ -30,9 +30,9 @@ impl AppConfig { pub fn new() -> AppConfig { AppConfig { tenant_id: None, - client_id: String::with_capacity(32), + client_id: Uuid::default(), authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, @@ -42,9 +42,9 @@ impl AppConfig { pub(crate) fn new_with_client_id(client_id: impl AsRef<str>) -> AppConfig { AppConfig { tenant_id: None, - client_id: client_id.as_ref().to_string(), + client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, @@ -57,9 +57,9 @@ impl AppConfig { ) -> AppConfig { AppConfig { tenant_id: Some(tenant_id.as_ref().to_string()), - client_id: client_id.as_ref().to_string(), + client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), authority: Authority::TenantId(tenant_id.as_ref().to_string()), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 74b95979..6ca2a45b 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -14,6 +14,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::env::VarError; use url::Url; +use uuid::Uuid; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { @@ -203,7 +204,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { AF::condition( - !value.client_id.is_empty(), + !value.client_id.to_string().is_empty(), "Client Id", "Client Id cannot be empty", )?; @@ -221,15 +222,12 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { Ok(ConfidentialClientApplicationBuilder { app_config: AppConfig { tenant_id: value.tenant_id, - client_id: value.client_id, + client_id: Uuid::try_parse(&value.client_id.to_string()).unwrap_or_default(), authority: value .aad_authority_audience .map(Authority::from) .unwrap_or_default(), - authority_url: value - .azure_cloud_instance - .map(AuthorityHost::AzureCloudInstance) - .unwrap_or_default(), + azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, @@ -332,7 +330,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { AF::condition( - !value.client_id.is_empty(), + !value.client_id.is_nil(), "client_id", "Client id cannot be empty", )?; @@ -355,10 +353,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { .aad_authority_audience .map(Authority::from) .unwrap_or_default(), - authority_url: value - .azure_cloud_instance - .map(AuthorityHost::AzureCloudInstance) - .unwrap_or_default(), + azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, @@ -378,7 +373,7 @@ mod test { #[should_panic] fn confidential_client_error_result_on_instance_and_aci() { ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), + client_id: Uuid::new_v4(), tenant_id: None, aad_authority_audience: None, instance: Some(Url::parse("https://login.microsoft.com").unwrap()), @@ -392,7 +387,7 @@ mod test { #[should_panic] fn confidential_client_error_result_on_tenant_id_and_aad_audience() { ConfidentialClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), + client_id: Uuid::new_v4(), tenant_id: Some("tenant_id".to_owned()), aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), instance: None, @@ -406,7 +401,7 @@ mod test { #[should_panic] fn public_client_error_result_on_instance_and_aci() { PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_string(), + client_id: Uuid::new_v4(), tenant_id: None, aad_authority_audience: None, instance: Some(Url::parse("https://login.microsoft.com").unwrap()), @@ -420,7 +415,7 @@ mod test { #[should_panic] fn public_client_error_result_on_tenant_id_and_aad_audience() { PublicClientApplicationBuilder::try_from(ApplicationOptions { - client_id: "client-id".to_owned(), + client_id: Uuid::new_v4(), tenant_id: Some("tenant_id".to_owned()), aad_authority_audience: Some(AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount), instance: None, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index 14426bcf..0760ba0d 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -13,6 +13,7 @@ use reqwest::IntoUrl; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; +use uuid::Uuid; /// Get the authorization url required to perform the initial authorization and redirect in the /// authorization code flow. @@ -66,9 +67,9 @@ impl AuthCodeAuthorizationUrlParameters { Ok(AuthCodeAuthorizationUrlParameters { app_config: AppConfig { tenant_id: None, - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref())?, authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), @@ -214,8 +215,8 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { } } - let client_id = self.app_config.client_id.trim(); - if client_id.is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result("client_id"); } @@ -231,7 +232,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { } serializer - .client_id(client_id) + .client_id(client_id.as_str()) .extend_scopes(self.scope.clone()) .authority(azure_cloud_instance, &self.app_config.authority); @@ -360,7 +361,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.parameters.app_config.client_id = client_id.as_ref().to_owned(); + self.parameters.app_config.client_id = + Uuid::try_parse(client_id.as_ref()).expect("Invalid Client Id - Must be a Uuid "); self } @@ -565,7 +567,7 @@ mod test { fn serialize_uri() { let authorizer = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .build(); @@ -577,7 +579,7 @@ mod test { fn url_with_host() { let authorizer = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .build(); @@ -589,7 +591,7 @@ mod test { fn response_mode_set() { let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(ResponseType::IdToken) .url() @@ -605,7 +607,7 @@ mod test { fn response_mode_not_set() { let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .url() .unwrap(); @@ -619,7 +621,7 @@ mod test { fn multi_response_type_set() { let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) .with_response_type(vec![ResponseType::IdToken, ResponseType::Code]) @@ -635,7 +637,7 @@ mod test { fn generate_nonce() { let url = AuthCodeAuthorizationUrlParameters::builder() .with_redirect_uri("https://localhost:8080") - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_nonce_generated() diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 715d41c8..f9ae6590 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -11,6 +11,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; +use uuid::Uuid; #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; @@ -70,10 +71,10 @@ impl AuthorizationCodeCertificateCredential { }; let app_config = AppConfig { - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), tenant_id: None, authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri, @@ -119,8 +120,8 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.app_config.client_id.trim(); - if client_id.is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId); } @@ -133,7 +134,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { } self.serializer - .client_id(client_id) + .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .extend_scopes(self.scope.clone()); @@ -203,7 +204,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { ) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -214,6 +215,10 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn authority(&self) -> Authority { self.app_config.authority.clone() } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } } #[derive(Clone)] diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index c24c6b1e..4937cabc 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -11,6 +11,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; +use uuid::Uuid; credential_builder!( AuthorizationCodeCredentialBuilder, @@ -90,9 +91,9 @@ impl AuthorizationCodeCredential { let app_config = AppConfig { tenant_id: Some(tenant_id.as_ref().to_owned()), - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref())?, authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.clone()), @@ -250,8 +251,8 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.client_id().clone(); - if client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -321,7 +322,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { ) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -329,9 +330,13 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + fn basic_auth(&self) -> Option<(String, String)> { Some(( - self.app_config.client_id.clone(), + self.app_config.client_id.to_string(), self.client_secret.clone(), )) } @@ -366,11 +371,12 @@ mod test { #[test] #[should_panic] fn authorization_code_missing_required_value() { + let uuid_value = Uuid::new_v4(); let mut credential_builder = AuthorizationCodeCredentialBuilder::new(); credential_builder .with_redirect_uri("https://localhost:8080") .unwrap() - .with_client_id("client_id") + .with_client_id(uuid_value.to_string()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id"); @@ -391,18 +397,18 @@ mod test { #[test] fn serialization() { + let uuid_value = Uuid::new_v4(); let mut credential_builder = AuthorizationCodeCredential::builder("code"); let mut credential = credential_builder .with_redirect_uri("https://localhost") .unwrap() - .with_client_id("client_id") + .with_client_id(uuid_value.to_string()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id") - .with_authorization_code("authorization_code") .build(); let map = credential.form_urlencode().unwrap(); - assert_eq!(map.get("client_id"), Some(&String::from("client_id"))) + assert_eq!(map.get("client_id"), Some(&uuid_value.to_string())) } } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 3272b197..caaa4f0c 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -8,6 +8,7 @@ use graph_error::{AuthorizationResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; credential_builder!( ClientAssertionCredentialBuilder, @@ -105,7 +106,7 @@ impl TokenCredentialExecutor for ClientAssertionCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.client_id().clone(); + let client_id = self.client_id().to_string(); if client_id.trim().is_empty() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -165,7 +166,7 @@ impl TokenCredentialExecutor for ClientAssertionCredential { }; } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -173,6 +174,10 @@ impl TokenCredentialExecutor for ClientAssertionCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + fn app_config(&self) -> &AppConfig { &self.app_config } diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index f34473fe..c81b0003 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -2,14 +2,8 @@ macro_rules! credential_builder_base { ($name:ident) => { impl $name { pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - if self.credential.app_config.client_id.is_empty() { - self.credential - .app_config - .client_id - .push_str(client_id.as_ref()); - } else { - self.credential.app_config.client_id = client_id.as_ref().to_owned(); - } + self.credential.app_config.client_id = + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); self } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 08ebab26..79e3c6aa 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -6,6 +6,7 @@ use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; @@ -52,10 +53,8 @@ impl ClientCertificateCredential { client_id: T, x509: &X509Certificate, ) -> anyhow::Result<ClientCertificateCredential> { - let mut builder = ClientCertificateCredentialBuilder::new(); - builder - .with_client_id(client_id.as_ref()) - .with_certificate(x509)?; + let mut builder = ClientCertificateCredentialBuilder::new(client_id.as_ref()); + builder.with_certificate(x509)?; Ok(builder.credential) } @@ -64,8 +63,8 @@ impl ClientCertificateCredential { self } - pub fn builder() -> ClientCertificateCredentialBuilder { - ClientCertificateCredentialBuilder::new() + pub fn builder<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder { + ClientCertificateCredentialBuilder::new(client_id) } pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { @@ -75,9 +74,9 @@ impl ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.app_config.authority); + .authority(azure_cloud_instance, &self.app_config.authority); let uri = self .serializer @@ -87,8 +86,8 @@ impl TokenCredentialExecutor for ClientCertificateCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.client_id().clone(); - if client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } @@ -147,7 +146,7 @@ impl TokenCredentialExecutor for ClientCertificateCredential { }; } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -155,6 +154,10 @@ impl TokenCredentialExecutor for ClientCertificateCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + fn app_config(&self) -> &AppConfig { &self.app_config } @@ -165,10 +168,10 @@ pub struct ClientCertificateCredentialBuilder { } impl ClientCertificateCredentialBuilder { - fn new() -> ClientCertificateCredentialBuilder { + fn new<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { - app_config: Default::default(), + app_config: AppConfig::new_with_client_id(client_id.as_ref()), scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), @@ -183,10 +186,18 @@ impl ClientCertificateCredentialBuilder { x509: &X509Certificate, app_config: AppConfig, ) -> anyhow::Result<ClientCertificateCredentialBuilder> { - let mut builder = ClientCertificateCredentialBuilder::new(); - builder.credential.app_config = app_config; - builder.with_certificate(x509)?; - Ok(builder) + let mut credential_builder = ClientCertificateCredentialBuilder { + credential: ClientCertificateCredential { + app_config, + scope: vec!["https://graph.microsoft.com/.default".into()], + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + refresh_token: None, + serializer: OAuthSerializer::new(), + }, + }; + credential_builder.with_certificate(x509)?; + Ok(credential_builder) } #[cfg(feature = "openssl")] @@ -230,21 +241,30 @@ impl From<ClientCertificateCredentialBuilder> for ClientCertificateCredential { mod test { use super::*; - static TEST_CLIENT_ID: &str = "671a21bd-b91b-8ri7-94cb-e2cea49f30e1"; - #[test] - fn credential_builder() { - let mut builder = ClientCertificateCredentialBuilder::new(); - builder.with_client_id(TEST_CLIENT_ID); - assert_eq!(builder.credential.app_config.client_id, TEST_CLIENT_ID); - - builder.with_client_id("123"); - assert_eq!(builder.credential.app_config.client_id, "123"); + fn test_uuid_fake() { + let client_id_uuid = Uuid::new_v4(); + let builder = ClientCertificateCredentialBuilder::new(client_id_uuid.to_string()); + assert_eq!(builder.credential.app_config.client_id, client_id_uuid); + } - builder.credential.app_config.client_id = "".into(); - assert!(builder.credential.app_config.client_id.is_empty()); + #[test] + #[should_panic] + fn test_123_uuid() { + let builder = ClientCertificateCredentialBuilder::new("123"); + assert_eq!( + builder.credential.app_config.client_id, + Uuid::try_parse("123").unwrap() + ); + } - builder.with_client_id(TEST_CLIENT_ID); - assert_eq!(builder.credential.app_config.client_id, TEST_CLIENT_ID); + #[test] + fn credential_builder() { + let builder = + ClientCertificateCredentialBuilder::new("4ef900be-dfd9-4da6-b224-0011e46c54dd"); + assert_eq!( + builder.credential.app_config.client_id.to_string(), + "4ef900be-dfd9-4da6-b224-0011e46c54dd" + ); } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index c30f25ce..caad400f 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -10,6 +10,7 @@ use graph_error::{AuthorizationFailure, AuthorizationResult}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; credential_builder!(ClientSecretCredentialBuilder, ConfidentialClientApplication); @@ -89,8 +90,8 @@ impl TokenCredentialExecutor for ClientSecretCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.client_id().clone(); - if client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId); } @@ -98,7 +99,10 @@ impl TokenCredentialExecutor for ClientSecretCredential { return AuthorizationFailure::result(OAuthParameter::ClientSecret); } - self.serializer.grant_type("client_credentials"); + self.serializer + .client_id(client_id.as_str()) + .client_secret(self.client_secret.as_str()) + .grant_type("client_credentials"); if self.scope.is_empty() { self.serializer @@ -107,11 +111,13 @@ impl TokenCredentialExecutor for ClientSecretCredential { self.serializer.extend_scopes(&self.scope); } + // Don't include ClientId and Client Secret in the fields for form url encode because + // Client Id and Client Secret are already included as basic auth. self.serializer .as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -119,9 +125,13 @@ impl TokenCredentialExecutor for ClientSecretCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + todo!() + } + fn basic_auth(&self) -> Option<(String, String)> { Some(( - self.app_config.client_id.clone(), + self.app_config.client_id.to_string(), self.client_secret.clone(), )) } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 95dd1023..84c7853a 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -15,6 +15,7 @@ use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; use wry::http::HeaderMap; /// Clients capable of maintaining the confidentiality of their credentials @@ -62,7 +63,7 @@ impl TokenCredentialExecutor for ConfidentialClientApplication { self.credential.form_urlencode() } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { self.credential.client_id() } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4a58abc0..e686f7e0 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -14,6 +14,7 @@ use std::time::Duration; use crate::identity::credentials::app_config::AppConfig; use url::Url; +use uuid::Uuid; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -167,8 +168,8 @@ impl DeviceCodeCredential { Ok(receiver) } - pub fn builder() -> DeviceCodeCredentialBuilder { - DeviceCodeCredentialBuilder::new() + pub fn builder(client_id: impl AsRef<str>) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder::new(client_id.as_ref()) } } @@ -193,13 +194,13 @@ impl TokenCredentialExecutor for DeviceCodeCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.app_config.client_id.trim(); - if client_id.is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } self.serializer - .client_id(client_id) + .client_id(client_id.as_str()) .extend_scopes(self.scope.clone()); if let Some(refresh_token) = self.refresh_token.as_ref() { @@ -252,7 +253,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { ) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -260,6 +261,10 @@ impl TokenCredentialExecutor for DeviceCodeCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + fn app_config(&self) -> &AppConfig { &self.app_config } @@ -271,10 +276,10 @@ pub struct DeviceCodeCredentialBuilder { } impl DeviceCodeCredentialBuilder { - fn new() -> DeviceCodeCredentialBuilder { + fn new<T: AsRef<str>>(client_id: T) -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { credential: DeviceCodeCredential { - app_config: Default::default(), + app_config: AppConfig::new_with_client_id(client_id.as_ref()), refresh_token: None, device_code: None, scope: vec![], @@ -350,9 +355,7 @@ mod test { #[test] #[should_panic] fn no_scope() { - let mut credential = DeviceCodeCredential::builder() - .with_client_id("CLIENT_ID") - .build(); + let mut credential = DeviceCodeCredential::builder("CLIENT_ID").build(); let _ = credential.form_urlencode().unwrap(); } diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index b9ffd3be..59f6bb02 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -10,6 +10,7 @@ use graph_error::AuthorizationResult; use std::collections::HashMap; use std::env::VarError; use url::Url; +use uuid::Uuid; const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; @@ -143,7 +144,11 @@ impl TokenCredentialExecutor for EnvironmentCredential { self.credential.form_urlencode() } - fn client_id(&self) -> &String { + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config().azure_cloud_instance + } + + fn client_id(&self) -> &Uuid { self.credential.client_id() } diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 737876ce..1d80aa2e 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -8,6 +8,7 @@ use reqwest::IntoUrl; use std::collections::HashMap; use url::form_urlencoded::Serializer; use url::Url; +use uuid::*; credential_builder_base!(ImplicitCredentialBuilder); @@ -112,8 +113,8 @@ impl ImplicitCredential { azure_authority_host: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - let client_id = self.app_config.client_id.trim(); - if client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result("client_id"); } @@ -122,7 +123,7 @@ impl ImplicitCredential { } serializer - .client_id(client_id) + .client_id(client_id.as_str()) .nonce(self.nonce.as_str()) .extend_scopes(self.scope.clone()) .authority(azure_authority_host, &self.app_config.authority); @@ -504,7 +505,7 @@ mod test { let url = ImplicitCredential::builder() .with_redirect_uri("http://localhost:8080") .unwrap() - .with_client_id("client_id") + .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_nonce_generated() diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 60830e80..b416d8d8 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -9,6 +9,7 @@ use reqwest::IntoUrl; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; +use uuid::Uuid; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your @@ -109,9 +110,9 @@ impl OpenIdAuthorizationUrl { Ok(OpenIdAuthorizationUrl { app_config: AppConfig { tenant_id: None, - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref())?, authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), @@ -174,8 +175,8 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - let client_id = self.app_config.client_id.as_str().trim(); - if client_id.is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result("client_id"); } @@ -191,7 +192,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } serializer - .client_id(client_id) + .client_id(client_id.as_str()) .extend_scopes(self.scope.clone()) .nonce(self.nonce.as_str()) .authority(azure_authority_host, &self.app_config.authority); @@ -308,7 +309,8 @@ impl OpenIdAuthorizationUrlBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.auth_url_parameters.app_config.client_id = client_id.as_ref().to_owned(); + self.auth_url_parameters.app_config.client_id = + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); self } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 2ff50812..7c45f931 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -11,6 +11,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; use url::Url; +use uuid::Uuid; credential_builder!(OpenIdCredentialBuilder, ConfidentialClientApplication); @@ -69,9 +70,9 @@ impl OpenIdCredential { Ok(OpenIdCredential { app_config: AppConfig { tenant_id: None, - client_id: client_id.as_ref().to_owned(), + client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), authority: Default::default(), - authority_url: Default::default(), + azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), @@ -126,8 +127,8 @@ impl TokenCredentialExecutor for OpenIdCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.app_config.client_id.trim(); - if client_id.is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -136,7 +137,7 @@ impl TokenCredentialExecutor for OpenIdCredential { } self.serializer - .client_id(client_id) + .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) .extend_scopes(self.scope.clone()); @@ -200,7 +201,7 @@ impl TokenCredentialExecutor for OpenIdCredential { ) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -208,9 +209,13 @@ impl TokenCredentialExecutor for OpenIdCredential { self.app_config.authority.clone() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + fn basic_auth(&self) -> Option<(String, String)> { Some(( - self.app_config.client_id.clone(), + self.app_config.client_id.to_string(), self.client_secret.clone(), )) } diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 68101605..80d9c369 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -11,6 +11,7 @@ use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; /// Clients incapable of maintaining the confidentiality of their credentials /// (e.g., clients executing on the device used by the resource owner, such as an @@ -59,7 +60,7 @@ impl TokenCredentialExecutor for PublicClientApplication { self.credential.form_urlencode() } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { self.credential.client_id() } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 96be9876..9dece46c 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -2,9 +2,10 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationResult, AF}; use std::collections::HashMap; use url::Url; +use uuid::Uuid; /// Allows an application to sign in the user by directly handling their password. /// Not recommended. ROPC can also be done using a client secret or assertion, @@ -60,8 +61,8 @@ impl ResourceOwnerPasswordCredential { } } - pub fn builder() -> ResourceOwnerPasswordCredentialBuilder { - ResourceOwnerPasswordCredentialBuilder::new() + pub fn builder<T: AsRef<str>>(client_id: T) -> ResourceOwnerPasswordCredentialBuilder { + ResourceOwnerPasswordCredentialBuilder::new(client_id) } } @@ -79,8 +80,8 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { - let client_id = self.app_config.client_id.trim(); - if client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); } @@ -93,7 +94,7 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { } self.serializer - .client_id(client_id) + .client_id(client_id.as_str()) .grant_type("password") .extend_scopes(self.scope.iter()); @@ -103,7 +104,7 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { ) } - fn client_id(&self) -> &String { + fn client_id(&self) -> &Uuid { &self.app_config.client_id } @@ -111,8 +112,8 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { self.app_config.authority.clone() } - fn basic_auth(&self) -> Option<(String, String)> { - Some((self.username.to_string(), self.password.to_string())) + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance } fn app_config(&self) -> &AppConfig { @@ -126,10 +127,10 @@ pub struct ResourceOwnerPasswordCredentialBuilder { } impl ResourceOwnerPasswordCredentialBuilder { - fn new() -> ResourceOwnerPasswordCredentialBuilder { + fn new<T: AsRef<str>>(client_id: T) -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { - app_config: Default::default(), + app_config: AppConfig::new_with_client_id(client_id.as_ref()), username: String::new(), password: String::new(), scope: vec![], @@ -139,14 +140,8 @@ impl ResourceOwnerPasswordCredentialBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - if self.credential.app_config.client_id.is_empty() { - self.credential - .app_config - .client_id - .push_str(client_id.as_ref()); - } else { - self.credential.app_config.client_id = client_id.as_ref().to_owned(); - } + self.credential.app_config.client_id = + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); self } @@ -185,17 +180,7 @@ impl ResourceOwnerPasswordCredentialBuilder { { return AF::msg_result( "tenant_id", - "Authority Azure Active Directory, common, and consumers are not supported authentication contexts for ROPC" - ); - } - - if authority.eq(&Authority::Common) - || authority.eq(&Authority::AzureActiveDirectory) - || authority.eq(&Authority::Consumers) - { - return AuthorizationFailure::msg_result( - "tenant_id", - "ADFS, common, and consumers are not supported authentication contexts for ROPC", + "AzureActiveDirectory, Common, and Consumers are not supported authentication contexts for ROPC" ); } @@ -214,12 +199,6 @@ impl ResourceOwnerPasswordCredentialBuilder { } } -impl Default for ResourceOwnerPasswordCredentialBuilder { - fn default() -> Self { - ResourceOwnerPasswordCredentialBuilder::new() - } -} - #[cfg(test)] mod test { use super::*; @@ -227,7 +206,7 @@ mod test { #[test] #[should_panic] fn fail_on_authority_common() { - let _ = ResourceOwnerPasswordCredential::builder() + let _ = ResourceOwnerPasswordCredential::builder(Uuid::new_v4().to_string()) .with_authority(Authority::Common) .unwrap() .build(); @@ -236,7 +215,7 @@ mod test { #[test] #[should_panic] fn fail_on_authority_adfs() { - let _ = ResourceOwnerPasswordCredential::builder() + let _ = ResourceOwnerPasswordCredential::builder(Uuid::new_v4().to_string()) .with_authority(Authority::AzureActiveDirectory) .unwrap() .build(); @@ -245,7 +224,7 @@ mod test { #[test] #[should_panic] fn fail_on_authority_consumers() { - let _ = ResourceOwnerPasswordCredential::builder() + let _ = ResourceOwnerPasswordCredential::builder(Uuid::new_v4().to_string()) .with_authority(Authority::Consumers) .unwrap() .build(); diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index ee15f7ec..0c79e6dd 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -10,17 +10,6 @@ pub enum ResponseType { StringSet(BTreeSet<String>), } -impl ResponseType { - pub fn try_from_set(response_types: &BTreeSet<ResponseType>) -> String { - dbg!(response_types); - - info!("{:#?}", &response_types); - let response_type_list: Vec<String> = - response_types.iter().map(|rt| rt.to_string()).collect(); - response_type_list.join(" ") - } -} - impl ToString for ResponseType { fn to_string(&self) -> String { match self { diff --git a/graph-oauth/src/identity/credentials/test/application_options/aad_options.json b/graph-oauth/src/identity/credentials/test/application_options/aad_options.json index 254c3313..840d64dc 100644 --- a/graph-oauth/src/identity/credentials/test/application_options/aad_options.json +++ b/graph-oauth/src/identity/credentials/test/application_options/aad_options.json @@ -1,4 +1,4 @@ { - "client_id": "1235", + "client_id": "a41c6b73-d9e1-4a47-84e1-77fa7e5a40e9", "aad_authority_audience": "PersonalMicrosoftAccount" } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index eb5c0fd4..18a3813d 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -9,17 +9,15 @@ use reqwest::tls::Version; use reqwest::ClientBuilder; use std::collections::HashMap; use url::Url; +use uuid::Uuid; #[async_trait] pub trait TokenCredentialExecutor { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url>; + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url>; fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; - fn client_id(&self) -> &String; + fn client_id(&self) -> &Uuid; fn authority(&self) -> Authority; - - fn azure_cloud_instance(&self) -> AzureCloudInstance { - AzureCloudInstance::AzurePublic - } + fn azure_cloud_instance(&self) -> AzureCloudInstance; fn basic_auth(&self) -> Option<(String, String)> { None diff --git a/graph-oauth/src/identity/credentials/token_credential_options.rs b/graph-oauth/src/identity/credentials/token_credential_options.rs index 103e05f6..a9ad3f27 100644 --- a/graph-oauth/src/identity/credentials/token_credential_options.rs +++ b/graph-oauth/src/identity/credentials/token_credential_options.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TokenCredentialOptions { - pub(crate) azure_authority_host: AzureCloudInstance, + pub azure_authority_host: AzureCloudInstance, pub extra_query_parameters: HashMap<String, String>, diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index d5124589..c06badc5 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -8,6 +8,7 @@ mod credentials; mod device_code; pub use allowed_host_validator::*; +pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; pub use authorization_serializer::*; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 4e8672c0..42bf208d 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -132,7 +132,7 @@ impl OAuthTestCredentials { } fn resource_owner_password_credential(self) -> ResourceOwnerPasswordCredential { - ResourceOwnerPasswordCredential::builder() + ResourceOwnerPasswordCredential::builder(self.client_id.as_str()) .with_tenant(self.tenant.as_str()) .with_client_id(self.client_id.as_str()) .with_username(self.username.as_str()) diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs new file mode 100644 index 00000000..443b5826 --- /dev/null +++ b/tests/todo_tasks_request.rs @@ -0,0 +1,40 @@ +use graph_core::resource::ResourceIdentity; +use serde::{Deserialize, Serialize}; +use test_tools::oauth_request::OAuthTestClient; +use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Task { + #[serde(alias = "displayName")] + display_name: String, + id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TodoListsTasks { + #[serde(alias = "@odata.context")] + odata_context: String, + value: Vec<Task>, +} + +#[tokio::test] +async fn list_users() { + std::env::set_var("GRAPH_TEST_ENV", "true"); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; + if let Some((id, client)) = + OAuthTestClient::graph_by_rid_async(ResourceIdentity::TodoListsTasks).await + { + let response = client + .user(id) + .todo() + .lists() + .list_lists() + .send() + .await + .unwrap(); + println!("{:#?}\n", response); + assert!(response.status().is_success()); + let body: TodoListsTasks = response.json().await.unwrap(); + assert!(body.value.len() >= 2); + } +} From c1ac02725f32bf69aaf386c84bc10b4f66ccdd1a Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 6 Sep 2023 02:13:06 -0400 Subject: [PATCH 037/118] Add more user examples --- examples/users/main.rs | 13 ++++ examples/users/todos/mod.rs | 1 + examples/users/todos/tasks.rs | 112 +++++++++++++++++++++++++++ examples/{users.rs => users/user.rs} | 19 ++--- 4 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 examples/users/main.rs create mode 100644 examples/users/todos/mod.rs create mode 100644 examples/users/todos/tasks.rs rename examples/{users.rs => users/user.rs} (90%) diff --git a/examples/users/main.rs b/examples/users/main.rs new file mode 100644 index 00000000..301bbf32 --- /dev/null +++ b/examples/users/main.rs @@ -0,0 +1,13 @@ +#![allow(dead_code, unused, unused_imports)] + +#[macro_use] +extern crate serde; + +/// Users todos and todos tasks. +mod todos; + +/// User specific APIs such as get and create users. +mod user; + +#[tokio::main] +async fn main() {} diff --git a/examples/users/todos/mod.rs b/examples/users/todos/mod.rs new file mode 100644 index 00000000..206c7d0e --- /dev/null +++ b/examples/users/todos/mod.rs @@ -0,0 +1 @@ +mod tasks; diff --git a/examples/users/todos/tasks.rs b/examples/users/todos/tasks.rs new file mode 100644 index 00000000..5f4dead6 --- /dev/null +++ b/examples/users/todos/tasks.rs @@ -0,0 +1,112 @@ +use futures::StreamExt; +use graph_error::GraphResult; +use graph_rs_sdk::Graph; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct ToDoListTask { + #[serde(rename = "displayName")] + pub display_name: String, +} + +impl ToDoListTask { + pub fn new(s: &str) -> ToDoListTask { + ToDoListTask { + display_name: s.to_string(), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TodoListTaskCollection { + value: VecDeque<ToDoListTask>, +} + +async fn list_tasks(user_id: &str, list_id: &str) -> GraphResult<()> { + let client = Graph::new("ACCESS_TOKEN"); + + let mut stream = client + .user(user_id) + .todo() + .list(list_id) + .tasks() + .list_tasks() + .paging() + .stream::<TodoListTaskCollection>(); + + while let Some(result) = stream.next().await { + let response = result?; + println!("{response:#?}"); + + let body = response.into_body()?; + println!("{body:#?}"); + } + + Ok(()) +} + +async fn create_task(user_id: &str, list_id: &str) -> GraphResult<()> { + let client = Graph::new("ACCESS_TOKEN"); + + let task = &serde_json::json!( + { + "title":"A new task", + "categories": ["Important"], + "linkedResources":[ + { + "webUrl":"http://microsoft.com", + "applicationName":"Microsoft", + "displayName":"Microsoft task" + } + ] + }); + + let response = client + .user(user_id) + .todo() + .list(list_id) + .tasks() + .create_tasks(&task) + .send() + .await?; + + println!("{response:#?}"); + + let body: serde_json::Value = response.json().await?; + println!("{body:#?}"); + + Ok(()) +} + +async fn create_task_using_me(list_id: &str) -> GraphResult<()> { + let client = Graph::new("ACCESS_TOKEN"); + + let task = &serde_json::json!( + { + "title":"A new task", + "categories": ["Important"], + "linkedResources":[ + { + "webUrl":"http://microsoft.com", + "applicationName":"Microsoft", + "displayName":"Microsoft task" + } + ] + }); + + let response = client + .me() + .todo() + .list(list_id) + .tasks() + .create_tasks(&task) + .send() + .await?; + + println!("{response:#?}"); + + let body: serde_json::Value = response.json().await?; + println!("{body:#?}"); + + Ok(()) +} diff --git a/examples/users.rs b/examples/users/user.rs similarity index 90% rename from examples/users.rs rename to examples/users/user.rs index 9aeca14f..e2f553b4 100644 --- a/examples/users.rs +++ b/examples/users/user.rs @@ -1,5 +1,3 @@ -use graph_rs_sdk::*; - // For more info on users see: https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0 // Work or school accounts must have the following permissions: User.ReadBasic.All, @@ -11,16 +9,10 @@ use graph_rs_sdk::*; // Delegate (Personal microsoft accounts) are not supported in the Graph API. -static USER_ID: &str = "USER_ID"; +use graph_error::GraphResult; +use graph_rs_sdk::Graph; -#[tokio::main] -async fn main() { - list_users().await.unwrap(); - get_user().await.unwrap(); - create_user().await; - update_user().await; - delete_user().await; -} +static USER_ID: &str = "USER_ID"; async fn list_users() -> GraphResult<()> { let client = Graph::new("ACCESS_TOKEN"); @@ -39,8 +31,7 @@ async fn get_user() -> GraphResult<()> { let client = Graph::new("ACCESS_TOKEN"); let response = client.user(USER_ID).get_user().send().await?; - - println!("{:#?}", &response); + println!("{response:#?}"); let body: serde_json::Value = response.json().await?; println!("{body:#?}"); @@ -69,8 +60,8 @@ async fn create_user() { }); let response = client.users().create_user(&user).send().await.unwrap(); + println!("{response:#?}"); - println!("{:#?}", &response); let body: serde_json::Value = response.json().await.unwrap(); println!("{body:#?}"); } From 5270b5217492da5e3f8e4c70cd4612482f1ce9fe Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 6 Sep 2023 03:01:41 -0400 Subject: [PATCH 038/118] Add client-id requirements to url builders --- .../credentials/application_builder.rs | 6 +- .../auth_code_authorization_url_parameters.rs | 56 +++++---- ...thorization_code_certificate_credential.rs | 10 +- .../authorization_code_credential.rs | 97 ++++++--------- .../client_assertion_credential.rs | 4 +- .../credentials/client_builder_impl.rs | 8 ++ .../client_certificate_credential.rs | 6 +- .../client_credentials_authorization_url.rs | 110 +++++++++++------- .../credentials/client_secret_credential.rs | 12 +- .../confidential_client_application.rs | 4 +- .../credentials/environment_credential.rs | 8 +- .../credentials/implicit_credential.rs | 4 +- .../legacy/code_flow_credential.rs | 4 +- .../credentials/open_id_authorization_url.rs | 8 +- .../credentials/open_id_credential.rs | 4 +- .../credentials/public_client_application.rs | 4 +- .../resource_owner_password_credential.rs | 4 +- .../credentials/token_credential_options.rs | 10 -- 18 files changed, 185 insertions(+), 174 deletions(-) diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 6ca2a45b..ac16211b 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -110,17 +110,17 @@ impl ConfidentialClientApplicationBuilder { } pub fn authorization_code_url_builder(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { - AuthCodeAuthorizationUrlParameterBuilder::new() + AuthCodeAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) } pub fn client_credentials_auth_url_builder( &mut self, ) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new() + ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) } pub fn openid_authorization_url_builder(&mut self) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new() + ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) } #[cfg(feature = "openssl")] diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index 0760ba0d..2ffbc7d7 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -87,8 +87,8 @@ impl AuthCodeAuthorizationUrlParameters { }) } - pub fn builder() -> AuthCodeAuthorizationUrlParameterBuilder { - AuthCodeAuthorizationUrlParameterBuilder::new() + pub fn builder<T: AsRef<str>>(client_id: T) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } pub fn url(&self) -> AuthorizationResult<Url> { @@ -97,9 +97,9 @@ impl AuthCodeAuthorizationUrlParameters { pub fn url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { - self.authorization_url_with_host(azure_authority_host) + self.authorization_url_with_host(azure_cloud_instance) } /// Get the nonce. @@ -328,19 +328,35 @@ pub struct AuthCodeAuthorizationUrlParameterBuilder { parameters: AuthCodeAuthorizationUrlParameters, } -impl Default for AuthCodeAuthorizationUrlParameterBuilder { - fn default() -> Self { - Self::new() +impl AuthCodeAuthorizationUrlParameterBuilder { + pub fn new<T: AsRef<str>>(client_id: T) -> AuthCodeAuthorizationUrlParameterBuilder { + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::Code); + AuthCodeAuthorizationUrlParameterBuilder { + parameters: AuthCodeAuthorizationUrlParameters { + app_config: AppConfig::new_with_client_id(client_id.as_ref()), + response_mode: None, + response_type, + nonce: None, + state: None, + scope: vec![], + prompt: None, + domain_hint: None, + login_hint: None, + code_challenge: None, + code_challenge_method: None, + }, + } } -} -impl AuthCodeAuthorizationUrlParameterBuilder { - pub fn new() -> AuthCodeAuthorizationUrlParameterBuilder { + pub(crate) fn new_with_app_config( + app_config: AppConfig, + ) -> AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { parameters: AuthCodeAuthorizationUrlParameters { - app_config: AppConfig::default(), + app_config, response_mode: None, response_type, nonce: None, @@ -565,9 +581,8 @@ mod test { #[test] fn serialize_uri() { - let authorizer = AuthCodeAuthorizationUrlParameters::builder() + let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .build(); @@ -577,9 +592,8 @@ mod test { #[test] fn url_with_host() { - let authorizer = AuthCodeAuthorizationUrlParameters::builder() + let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .build(); @@ -589,9 +603,8 @@ mod test { #[test] fn response_mode_set() { - let url = AuthCodeAuthorizationUrlParameters::builder() + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(ResponseType::IdToken) .url() @@ -605,9 +618,8 @@ mod test { #[test] fn response_mode_not_set() { - let url = AuthCodeAuthorizationUrlParameters::builder() + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .url() .unwrap(); @@ -619,9 +631,8 @@ mod test { #[test] fn multi_response_type_set() { - let url = AuthCodeAuthorizationUrlParameters::builder() + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) .with_response_type(vec![ResponseType::IdToken, ResponseType::Code]) @@ -635,9 +646,8 @@ mod test { #[test] fn generate_nonce() { - let url = AuthCodeAuthorizationUrlParameters::builder() + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") - .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_nonce_generated() diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index f9ae6590..b3ff5247 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -101,16 +101,18 @@ impl AuthorizationCodeCertificateCredential { ) } - pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { - AuthCodeAuthorizationUrlParameterBuilder::new() + pub fn authorization_url_builder<T: AsRef<str>>( + client_id: T, + ) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } } #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority()); + .authority(azure_cloud_instance, &self.authority()); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 4937cabc..f9205dc3 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,8 +1,8 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AuthCodeAuthorizationUrlParameters, Authority, AzureCloudInstance, - ConfidentialClientApplication, ProofKeyForCodeExchange, TokenCredentialExecutor, + Authority, AzureCloudInstance, ConfidentialClientApplication, ProofKeyForCodeExchange, + TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; @@ -45,8 +45,6 @@ pub struct AuthorizationCodeCredential { /// specification. The Basic auth pattern of instead providing credentials in the Authorization /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, - /// The same redirect_uri value that was used to acquire the authorization_code. - pub(crate) redirect_uri: Url, /// A space-separated list of scopes. The scopes must all be from a single resource, /// along with OIDC scopes (profile, openid, email). For more information, see Permissions /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension @@ -72,7 +70,6 @@ impl AuthorizationCodeCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - redirect_uri: Url::parse("http://localhost").expect("Internal Error - please report"), scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), @@ -96,7 +93,7 @@ impl AuthorizationCodeCredential { azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri.clone()), + redirect_uri: Some(redirect_uri), }; Ok(AuthorizationCodeCredential { @@ -104,7 +101,6 @@ impl AuthorizationCodeCredential { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - redirect_uri, scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), @@ -115,12 +111,17 @@ impl AuthorizationCodeCredential { self.refresh_token = Some(refresh_token.as_ref().to_owned()); } - pub fn builder(authorization_code: impl AsRef<str>) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::builder(authorization_code) + pub fn builder<T: AsRef<str>, U: AsRef<str>>( + client_id: T, + authorization_code: U, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new(client_id, authorization_code) } - pub fn authorization_url_builder() -> AuthCodeAuthorizationUrlParameterBuilder { - AuthCodeAuthorizationUrlParameterBuilder::new() + pub fn authorization_url_builder<T: AsRef<str>>( + client_id: T, + ) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } } @@ -130,31 +131,16 @@ pub struct AuthorizationCodeCredentialBuilder { } impl AuthorizationCodeCredentialBuilder { - fn new() -> AuthorizationCodeCredentialBuilder { - Self { - credential: AuthorizationCodeCredential { - app_config: Default::default(), - authorization_code: None, - refresh_token: None, - client_secret: String::new(), - redirect_uri: Url::parse("http://localhost") - .expect("Internal Error - please report"), - scope: vec![], - code_verifier: None, - serializer: OAuthSerializer::new(), - }, - } - } - - fn builder(authorization_code: impl AsRef<str>) -> AuthorizationCodeCredentialBuilder { + pub fn new<T: AsRef<str>, U: AsRef<str>>( + client_id: T, + authorization_code: U, + ) -> AuthorizationCodeCredentialBuilder { Self { credential: AuthorizationCodeCredential { - app_config: Default::default(), + app_config: AppConfig::new_with_client_id(client_id.as_ref()), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: String::new(), - redirect_uri: Url::parse("http://localhost") - .expect("Internal Error - please report"), scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), @@ -166,18 +152,12 @@ impl AuthorizationCodeCredentialBuilder { app_config: AppConfig, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { - let redirect_uri = app_config - .redirect_uri - .clone() - .unwrap_or(Url::parse("http://localhost").expect("Internal Error - please report")); - Self { credential: AuthorizationCodeCredential { app_config, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: String::new(), - redirect_uri, scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), @@ -198,7 +178,7 @@ impl AuthorizationCodeCredentialBuilder { /// Defaults to http://localhost pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { - self.credential.redirect_uri = redirect_uri.into_url()?; + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); Ok(self) } @@ -221,16 +201,6 @@ impl AuthorizationCodeCredentialBuilder { } } -impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCredentialBuilder { - fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { - let mut builder = AuthorizationCodeCredentialBuilder::new(); - builder.credential.app_config = value.app_config; - builder.with_scope(value.scope); - - builder - } -} - impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { fn from(credential: AuthorizationCodeCredential) -> Self { AuthorizationCodeCredentialBuilder { credential } @@ -239,9 +209,9 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority()); + .authority(azure_cloud_instance, &self.authority()); let uri = self .serializer @@ -291,10 +261,13 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { ); } + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { + self.serializer.redirect_uri(redirect_uri.as_str()); + } + self.serializer .authorization_code(authorization_code.as_ref()) - .grant_type("authorization_code") - .redirect_uri(self.redirect_uri.as_str()); + .grant_type("authorization_code"); if let Some(code_verifier) = self.code_verifier.as_ref() { self.serializer.code_verifier(code_verifier.as_str()); @@ -352,7 +325,7 @@ mod test { #[test] fn with_tenant_id_common() { - let credential = AuthorizationCodeCredential::builder("code") + let credential = AuthorizationCodeCredential::builder(Uuid::new_v4().to_string(), "code") .with_authority(Authority::TenantId("common".into())) .build(); @@ -361,7 +334,7 @@ mod test { #[test] fn with_tenant_id_adfs() { - let credential = AuthorizationCodeCredential::builder("code") + let credential = AuthorizationCodeCredential::builder(Uuid::new_v4().to_string(), "code") .with_authority(Authority::AzureDirectoryFederatedServices) .build(); @@ -371,12 +344,11 @@ mod test { #[test] #[should_panic] fn authorization_code_missing_required_value() { - let uuid_value = Uuid::new_v4(); - let mut credential_builder = AuthorizationCodeCredentialBuilder::new(); + let mut credential_builder = + AuthorizationCodeCredentialBuilder::new(Uuid::new_v4().to_string(), "code"); credential_builder .with_redirect_uri("https://localhost:8080") .unwrap() - .with_client_id(uuid_value.to_string()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id"); @@ -387,7 +359,8 @@ mod test { #[test] #[should_panic] fn required_value_missing_client_id() { - let mut credential_builder = AuthorizationCodeCredential::builder("code"); + let mut credential_builder = + AuthorizationCodeCredential::builder(Uuid::default().to_string(), "code"); credential_builder .with_authorization_code("code") .with_refresh_token("token"); @@ -397,18 +370,18 @@ mod test { #[test] fn serialization() { - let uuid_value = Uuid::new_v4(); - let mut credential_builder = AuthorizationCodeCredential::builder("code"); + let uuid_value = Uuid::new_v4().to_string(); + let mut credential_builder = + AuthorizationCodeCredential::builder(uuid_value.clone(), "code"); let mut credential = credential_builder .with_redirect_uri("https://localhost") .unwrap() - .with_client_id(uuid_value.to_string()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id") .build(); let map = credential.form_urlencode().unwrap(); - assert_eq!(map.get("client_id"), Some(&uuid_value.to_string())) + assert_eq!(map.get("client_id"), Some(&uuid_value)) } } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index caaa4f0c..241d369f 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -94,9 +94,9 @@ impl ClientAssertionCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority()); + .authority(azure_cloud_instance, &self.authority()); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index c81b0003..4dd735cc 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -23,6 +23,14 @@ macro_rules! credential_builder_base { self } + pub fn with_azure_cloud_instance( + &mut self, + azure_cloud_instance: AzureCloudInstance, + ) -> &mut Self { + self.credential.app_config.azure_cloud_instance = azure_cloud_instance; + self + } + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( &mut self, scope: I, diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 79e3c6aa..7e3bfdd9 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -67,8 +67,10 @@ impl ClientCertificateCredential { ClientCertificateCredentialBuilder::new(client_id) } - pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new() + pub fn authorization_url_builder<T: AsRef<str>>( + client_id: T, + ) -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new(client_id) } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 66fe8e7a..68802e5c 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,58 +1,74 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance}; use graph_error::{AuthorizationFailure, AuthorizationResult}; +use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; +use uuid::Uuid; #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrl { /// The client (application) ID of the service principal - pub(crate) client_id: String, - pub(crate) redirect_uri: String, + pub(crate) app_config: AppConfig, pub(crate) state: Option<String>, - pub(crate) authority: Authority, } impl ClientCredentialsAuthorizationUrl { - pub fn new<T: AsRef<str>>(client_id: T, redirect_uri: T) -> ClientCredentialsAuthorizationUrl { - ClientCredentialsAuthorizationUrl { - client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), + pub fn new<T: AsRef<str>, U: IntoUrl>( + client_id: T, + redirect_uri: U, + ) -> AuthorizationResult<ClientCredentialsAuthorizationUrl> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; + + Ok(ClientCredentialsAuthorizationUrl { + app_config: AppConfig { + tenant_id: None, + client_id: Uuid::try_parse(client_id.as_ref())?, + authority: Default::default(), + azure_cloud_instance: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: Some(redirect_uri), + }, state: None, - authority: Default::default(), - } + }) } - pub fn builder() -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new() + pub fn builder<T: AsRef<str>>(client_id: T) -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new(client_id) } pub fn url(&self) -> AuthorizationResult<Url> { - self.url_with_host(&AzureCloudInstance::AzurePublic) + self.url_with_host(&self.app_config.azure_cloud_instance) } pub fn url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); - if self.client_id.trim().is_empty() { + let client_id = self.app_config.client_id.to_string(); + if client_id.trim().is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } - if self.redirect_uri.trim().is_empty() { + if self.app_config.redirect_uri.is_none() { return AuthorizationFailure::result(OAuthParameter::RedirectUri.alias()); } - serializer - .client_id(self.client_id.as_str()) - .redirect_uri(self.redirect_uri.as_str()); + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { + serializer.redirect_uri(redirect_uri.as_str()); + } + + serializer.client_id(client_id.as_str()); if let Some(state) = self.state.as_ref() { serializer.state(state.as_ref()); } - serializer.authority_admin_consent(azure_authority_host, &self.authority); + serializer.authority_admin_consent(azure_cloud_instance, &self.app_config.authority); let mut encoder = Serializer::new(String::new()); serializer.form_encode_credentials( @@ -81,59 +97,67 @@ impl ClientCredentialsAuthorizationUrl { } pub struct ClientCredentialsAuthorizationUrlBuilder { - client_credentials_authorization_url: ClientCredentialsAuthorizationUrl, + parameters: ClientCredentialsAuthorizationUrl, } impl ClientCredentialsAuthorizationUrlBuilder { - pub fn new() -> Self { + pub fn new<T: AsRef<str>>(client_id: T) -> Self { Self { - client_credentials_authorization_url: ClientCredentialsAuthorizationUrl { - client_id: String::new(), - redirect_uri: String::new(), + parameters: ClientCredentialsAuthorizationUrl { + app_config: AppConfig::new_with_client_id(client_id), state: None, - authority: Default::default(), }, } } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.client_credentials_authorization_url.client_id = client_id.as_ref().to_owned(); - self + pub fn new_with_app_config(app_config: AppConfig) -> Self { + Self { + parameters: ClientCredentialsAuthorizationUrl { + app_config, + state: None, + }, + } } - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.client_credentials_authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); - self + pub fn with_client_id<T: AsRef<str>>( + &mut self, + client_id: T, + ) -> AuthorizationResult<&mut Self> { + self.parameters.app_config.client_id = Uuid::try_parse(client_id.as_ref())?; + Ok(self) + } + + pub fn with_redirect_uri<T: IntoUrl>( + &mut self, + redirect_uri: T, + ) -> AuthorizationResult<&mut Self> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; + self.parameters.app_config.redirect_uri = Some(redirect_uri); + Ok(self) } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.client_credentials_authorization_url.authority = - Authority::TenantId(tenant.as_ref().to_owned()); + self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.client_credentials_authorization_url.authority = authority.into(); + self.parameters.app_config.authority = authority.into(); self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.client_credentials_authorization_url.state = Some(state.as_ref().to_owned()); + self.parameters.state = Some(state.as_ref().to_owned()); self } pub fn build(&self) -> ClientCredentialsAuthorizationUrl { - self.client_credentials_authorization_url.clone() + self.parameters.clone() } pub fn url(&self) -> AuthorizationResult<Url> { - self.client_credentials_authorization_url.url() - } -} - -impl Default for ClientCredentialsAuthorizationUrlBuilder { - fn default() -> Self { - ClientCredentialsAuthorizationUrlBuilder::new() + self.parameters.url() } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index caad400f..a6b5a9a0 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -68,16 +68,18 @@ impl ClientSecretCredential { } } - pub fn authorization_url_builder() -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new() + pub fn authorization_url_builder<T: AsRef<str>>( + client_id: T, + ) -> ClientCredentialsAuthorizationUrlBuilder { + ClientCredentialsAuthorizationUrlBuilder::new(client_id) } } #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.authority()); + .authority(azure_cloud_instance, &self.authority()); let uri = self.serializer @@ -126,7 +128,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { } fn azure_cloud_instance(&self) -> AzureCloudInstance { - todo!() + self.app_config.azure_cloud_instance } fn basic_auth(&self) -> Option<(String, String)> { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 84c7853a..cdfd5b47 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -55,8 +55,8 @@ impl ConfidentialClientApplication { #[async_trait] impl TokenCredentialExecutor for ConfidentialClientApplication { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_authority_host) + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.credential.uri(azure_cloud_instance) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 59f6bb02..0e674586 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -126,8 +126,8 @@ impl EnvironmentCredential { } impl AuthorizationSerializer for EnvironmentCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_authority_host) + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.credential.uri(azure_cloud_instance) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -136,8 +136,8 @@ impl AuthorizationSerializer for EnvironmentCredential { } impl TokenCredentialExecutor for EnvironmentCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_authority_host) + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + self.credential.uri(azure_cloud_instance) } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 1d80aa2e..915f2b5d 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -110,7 +110,7 @@ impl ImplicitCredential { pub fn url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); @@ -126,7 +126,7 @@ impl ImplicitCredential { .client_id(client_id.as_str()) .nonce(self.nonce.as_str()) .extend_scopes(self.scope.clone()) - .authority(azure_authority_host, &self.app_config.authority); + .authority(azure_cloud_instance, &self.app_config.authority); let response_types: Vec<String> = self.response_type.iter().map(|s| s.to_string()).collect(); diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs index 594eef9f..71126ba7 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs @@ -59,9 +59,9 @@ impl CodeFlowCredential { } impl AuthorizationSerializer for CodeFlowCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &Authority::Common); + .authority(azure_cloud_instance, &Authority::Common); if self.refresh_token.is_none() { let uri = self.serializer.get(OAuthParameter::TokenUrl).ok_or( diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index b416d8d8..4a918260 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -144,9 +144,9 @@ impl OpenIdAuthorizationUrl { pub fn url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { - self.authorization_url_with_host(azure_authority_host) + self.authorization_url_with_host(azure_cloud_instance) } /// Get the nonce. @@ -171,7 +171,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { fn authorization_url_with_host( &self, - azure_authority_host: &AzureCloudInstance, + azure_cloud_instance: &AzureCloudInstance, ) -> AuthorizationResult<Url> { let mut serializer = OAuthSerializer::new(); @@ -195,7 +195,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { .client_id(client_id.as_str()) .extend_scopes(self.scope.clone()) .nonce(self.nonce.as_str()) - .authority(azure_authority_host, &self.app_config.authority); + .authority(azure_cloud_instance, &self.app_config.authority); if self.response_type.is_empty() { serializer.response_type("code"); diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 7c45f931..401b195f 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -107,9 +107,9 @@ impl OpenIdCredential { #[async_trait] impl TokenCredentialExecutor for OpenIdCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.app_config.authority); + .authority(azure_cloud_instance, &self.app_config.authority); if self.refresh_token.is_none() { let uri = self diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 80d9c369..40e0adf8 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -77,8 +77,8 @@ impl TokenCredentialExecutor for PublicClientApplication { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let azure_authority_host = self.azure_cloud_instance(); - let uri = self.credential.uri(&azure_authority_host)?; + let azure_cloud_instance = self.azure_cloud_instance(); + let uri = self.credential.uri(&azure_cloud_instance)?; let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 9dece46c..edbc43b2 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -68,9 +68,9 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { - fn uri(&mut self, azure_authority_host: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { self.serializer - .authority(azure_authority_host, &self.app_config.authority); + .authority(azure_cloud_instance, &self.app_config.authority); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/token_credential_options.rs b/graph-oauth/src/identity/credentials/token_credential_options.rs index a9ad3f27..b250c87c 100644 --- a/graph-oauth/src/identity/credentials/token_credential_options.rs +++ b/graph-oauth/src/identity/credentials/token_credential_options.rs @@ -1,15 +1,5 @@ -use crate::identity::AzureCloudInstance; -use reqwest::header::HeaderMap; -use std::collections::HashMap; - #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TokenCredentialOptions { - pub azure_authority_host: AzureCloudInstance, - - pub extra_query_parameters: HashMap<String, String>, - - pub extra_header_parameters: HeaderMap, - /// Specifies if the token request will ignore the access token in the token cache /// and will attempt to acquire a new access token. pub force_refresh: bool, From d119c84dcb5c164af9e6df43ec78b08e39c695b7 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 21 Sep 2023 00:35:15 -0400 Subject: [PATCH 039/118] Add device code executor for polling device codes and update examples --- .../auth_code_grant/auth_code_grant_pkce.rs | 18 +- .../auth_code_grant/auth_code_grant_secret.rs | 3 +- .../client_credentials_admin_consent.rs | 7 +- examples/oauth/device_code.rs | 19 +- examples/oauth/main.rs | 8 +- examples/oauth/openid_connect/mod.rs | 4 +- ..._server.rs => openid_connect_form_post.rs} | 11 +- examples/oauth_authorization_url/README.md | 77 ++++- examples/oauth_authorization_url/main.rs | 24 +- examples/oauth_certificate/main.rs | 21 +- examples/users/todos/tasks.rs | 4 +- graph-error/src/authorization_failure.rs | 13 + graph-error/src/error.rs | 10 + graph-error/src/lib.rs | 1 + .../src/http/response_converter.rs | 74 +++-- .../src/identity/credentials/app_config.rs | 4 + .../credentials/application_builder.rs | 13 +- .../auth_code_authorization_url_parameters.rs | 1 + ...thorization_code_certificate_credential.rs | 1 + .../authorization_code_credential.rs | 9 +- .../client_credentials_authorization_url.rs | 1 + .../confidential_client_application.rs | 34 +- .../credentials/device_code_credential.rs | 294 +++++++++++------- .../credentials/open_id_authorization_url.rs | 16 +- .../credentials/open_id_credential.rs | 1 + .../credentials/token_credential_executor.rs | 30 +- graph-oauth/src/identity/device_code.rs | 16 +- 27 files changed, 470 insertions(+), 244 deletions(-) rename examples/oauth/openid_connect/{openid_local_server.rs => openid_connect_form_post.rs} (92%) diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index 8035ecef..de5c4d84 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,3 +1,4 @@ +use graph_oauth::identity::ResponseType; use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, @@ -32,8 +33,7 @@ pub struct AccessCode { // url and query needed to get an authorization code and opens the default system // web browser to this Url. fn authorization_sign_in() { - let url = AuthorizationCodeCredential::authorization_url_builder() - .with_client_id(CLIENT_ID) + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_scope(vec!["user.read"]) .with_redirect_uri("http://localhost:8000/redirect") .with_pkce(&PKCE) @@ -56,13 +56,13 @@ async fn handle_redirect( println!("{:#?}", access_code.code); let authorization_code = access_code.code; - let mut confidential_client = AuthorizationCodeCredential::builder(authorization_code) - .with_client_id(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_redirect_uri("http://localhost:8000/redirect") - .unwrap() - .with_pkce(&PKCE) - .build(); + let mut confidential_client = + AuthorizationCodeCredential::builder(CLIENT_ID, authorization_code) + .with_client_secret(CLIENT_SECRET) + .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() + .with_pkce(&PKCE) + .build(); // Returns reqwest::Response let response = confidential_client.execute_async().await.unwrap(); diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index 1fcad3e7..cba98cea 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -19,8 +19,7 @@ pub struct AccessCode { } pub fn authorization_sign_in() { - let url = AuthorizationCodeCredential::authorization_url_builder() - .with_client_id(CLIENT_ID) + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_redirect_uri(REDIRECT_URI) .with_scope(vec![SCOPE]) .url() diff --git a/examples/oauth/client_credentials/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/client_credentials_admin_consent.rs index d38c1e3e..d4abc563 100644 --- a/examples/oauth/client_credentials/client_credentials_admin_consent.rs +++ b/examples/oauth/client_credentials/client_credentials_admin_consent.rs @@ -28,7 +28,7 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // Paste the URL into a browser and have the admin sign in and approve the admin consent. fn get_admin_consent_url() -> AuthorizationResult<url::Url> { - let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI); + let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI)?; authorization_credential.url() } @@ -36,9 +36,8 @@ fn get_admin_consent_url() -> AuthorizationResult<url::Url> { // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. fn get_admin_consent_url_from_builder() -> AuthorizationResult<url::Url> { - let authorization_credential = ClientCredentialsAuthorizationUrl::builder() - .with_client_id(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI) + let authorization_credential = ClientCredentialsAuthorizationUrl::builder(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI)? .with_state("123") .with_tenant("tenant_id") .build(); diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 8aa6d77e..8662b71d 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -13,11 +13,20 @@ static CLIENT_ID: &str = "<CLIENT_ID>"; static TENANT: &str = "<TENANT>"; // Make the call to get a device code from the user. -fn get_auth_call_for_device_code() { - let mut public_client = PublicClientApplication::builder(CLIENT_ID) - .with_device_code_builder() - .with_scope(["User.Read"]) - .with_tenant(TENANT); + +// Poll the device code endpoint to get the code and a url that the user must +// go to in order to enter the code. Polling will continue until either the user +// has entered the and an access token is returned or an error happens. +fn poll_device_code() { + let mut device_executor = PublicClientApplication::builder(CLIENT_ID) + .with_device_authorization_executor() + .with_scope(vec!["User.Read"]) + .poll() + .unwrap(); + + while let Ok(response) = device_executor.recv() { + println!("{:#?}", response); + } } fn get_token(device_code: &str) { diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 25268c95..c8c851a6 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -57,8 +57,7 @@ async fn main() {} async fn auth_code_grant(authorization_code: &str) { let pkce = ProofKeyForCodeExchange::generate().unwrap(); - let credential = AuthorizationCodeCredential::builder(authorization_code) - .with_client_id("CLIENT_ID") + let credential = AuthorizationCodeCredential::builder("CLIENT_ID", authorization_code) .with_client_secret("CLIENT_SECRET") .with_redirect_uri("http://localhost:8000/redirect") .unwrap() @@ -76,8 +75,9 @@ async fn auth_code_grant(authorization_code: &str) { // Client Credentials Grant async fn client_credentials() { - let client_secret_credential = ClientSecretCredential::new("CLIENT_ID", "CLIENT_SECRET"); - let mut confidential_client = ConfidentialClientApplication::from(client_secret_credential); + let mut confidential_client = ConfidentialClientApplication::builder("CLIENT_ID") + .with_client_secret("CLIENT_SECRET") + .build(); let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); diff --git a/examples/oauth/openid_connect/mod.rs b/examples/oauth/openid_connect/mod.rs index bc61d951..a23e133a 100644 --- a/examples/oauth/openid_connect/mod.rs +++ b/examples/oauth/openid_connect/mod.rs @@ -1,3 +1,3 @@ -mod openid_local_server; +mod openid_connect_form_post; -pub use openid_local_server::*; +pub use openid_connect_form_post::*; diff --git a/examples/oauth/openid_connect/openid_local_server.rs b/examples/oauth/openid_connect/openid_connect_form_post.rs similarity index 92% rename from examples/oauth/openid_connect/openid_local_server.rs rename to examples/oauth/openid_connect/openid_connect_form_post.rs index d4bf9120..a34b4cbb 100644 --- a/examples/oauth/openid_connect/openid_local_server.rs +++ b/examples/oauth/openid_connect/openid_connect_form_post.rs @@ -23,6 +23,9 @@ use url::Url; /// OAuth-enabled applications by using a security token called an ID token. use warp::Filter; +// Use the form post form post response mode when listening on a server instead +// of the URL query because the the query does not get sent to servers. + // The client id and client secret must be changed before running this example. static CLIENT_ID: &str = ""; static CLIENT_SECRET: &str = ""; @@ -30,16 +33,16 @@ static TENANT_ID: &str = ""; static REDIRECT_URI: &str = "http://localhost:8000/redirect"; -fn openid_authorization_url(client_id: &str, client_secret: &str) -> anyhow::Result<Url> { +fn openid_authorization_url() -> anyhow::Result<Url> { Ok(OpenIdCredential::authorization_url_builder()? .with_client_id(CLIENT_ID) .with_tenant(TENANT_ID) //.with_default_scope()? - .with_redirect_uri("http://localhost:8000/redirect")? + .with_redirect_uri(REDIRECT_URI)? .with_response_mode(ResponseMode::FormPost) .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) - .with_state(REDIRECT_URI) + .with_state("1234") .extend_scope(vec!["User.Read", "User.ReadWrite"]) .build() .url()?) @@ -109,7 +112,7 @@ pub async fn start_server_main() { .and_then(handle_redirect) .with(warp::trace::named("executor")); - let url = openid_authorization_url(CLIENT_ID, CLIENT_SECRET).unwrap(); + let url = openid_authorization_url().unwrap(); webbrowser::open(url.as_ref()); warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; diff --git a/examples/oauth_authorization_url/README.md b/examples/oauth_authorization_url/README.md index ed0701cf..2c6d37e1 100644 --- a/examples/oauth_authorization_url/README.md +++ b/examples/oauth_authorization_url/README.md @@ -1,13 +1,86 @@ # Building an Authorization URL The authorization request is the initial request to sign in where the user -is taken to the sign in page and enters their credentials. +is taken to the sign-in page and enters their credentials. -If successful the user will be redirected back to your app and the authorization +If successful, the user will be redirected back to your app and the authorization code will be in the query of the URL. ## Examples +### Authorization Code Grant + +* **Tenant** + * Required. Defaults to `common` when not provided to the client and is automatically set by the client. + * Definition: You can use the {tenant} value in the path of the request to control who can sign in to the application. + The allowed values are common, organizations, consumers, and tenant identifiers. +* **Client Id** - + * Required. + * Definition: The Application (client) ID that the Azure portal – App registrations experience assigned to your app. +* **Response Type** + * Required. Defaults to `code` in the client and is automatically set by the client. + * Definition: Must include `code` for the authorization code flow. Can also include `id_token` or `token` if using the hybrid flow. +* **Redirect URI** + * Required. Defaults to `http://localhost` in the client and is automatically set by the client. + * Definition: The redirect_uri of your app, where authentication responses can be sent and received by your app. + It must exactly match one of the redirect URIs you registered in the portal, except it must be URL-encoded. +* **Scope** + * Required: Not automatically set by the client. Callers will want to make sure at least one scope is provided to the client. + * Definition: A space-separated list of scopes that you want the user to consent to. + + +```rust +use graph_rs_sdk::oauth::AuthorizationCodeCredential; + +fn auth_code_flow_authorization_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) { + let url = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_redirect_uri(redirect_uri) + .with_scope(scope) + .url() + .unwrap(); + + // web browser crate opens default browser. + webbrowser::open(url.as_str()).unwrap(); +} + +``` + +### Authorization Code Grant With Proof Key For Code Exchange (PKCE) + +```rust +use graph_rs_sdk::oauth::{AuthorizationCodeCredential, ProofKeyForCodeExchange}; + +fn auth_code_pkce_authorization_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) { + let pkce = ProofKeyForCodeExchange::generate().unwrap(); + + let url = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_redirect_uri(redirect_uri) + .with_scope(scope) + .with_pkce(&pkce) + .url() + .unwrap(); +} +``` + +### [Authorization Code Hybrid Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-id-token-as-well-or-hybrid-flow) + +Easy to use methods are provided for parameters that can be changed for the auth code hybrid flow such as ResponseType. + +If your wanting to use openid connect consider using the `OpenIdCredential` which is a dedicated type that +is preconfigured to make it easy to perform the openid connect flow. + +```rust +fn auth_code_flow_authorization_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) { + let url = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_redirect_uri(redirect_uri) + .with_scope(scope) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .url() + .unwrap(); +} + +``` + ### OpenId Connect diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index 46e40f5b..863e6b2e 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -22,25 +22,28 @@ fn main() {} static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static SCOPE: &str = "User.Read"; // or pass more values to vec![] below // Authorization Code Grant Auth URL Builder pub fn auth_code_grant_authorization() { - let url = AuthorizationCodeCredential::authorization_url_builder() - .with_client_id(CLIENT_ID) - .with_redirect_uri("http://localhost:8000/redirect") - .with_scope(vec!["user.read"]) + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) .url() .unwrap(); - // web browser crate in dev dependencies will open to default browser in the system. + // web browser crate opens default browser. webbrowser::open(url.as_str()).unwrap(); } // Authorization Code Grant PKCE -// This example shows how to use a code_challenge and code_verifier +// This example shows how to generate a code_challenge and code_verifier // to perform the authorization code grant flow with proof key for -// code exchange (PKCE). +// code exchange (PKCE) otherwise known as an assertion. +// +// You can also use values of your own for the assertion. // // For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow // And the PKCE RFC: https://tools.ietf.org/html/rfc7636 @@ -52,10 +55,9 @@ pub fn auth_code_grant_authorization() { fn auth_code_grant_pkce_authorization() { let pkce = ProofKeyForCodeExchange::generate().unwrap(); - let url = AuthorizationCodeCredential::authorization_url_builder() - .with_client_id(CLIENT_ID) - .with_scope(vec!["user.read"]) - .with_redirect_uri("http://localhost:8000/redirect") + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) .with_pkce(&pkce) .url() .unwrap(); diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 9f5ae731..cb346e3f 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -45,6 +45,10 @@ static CLIENT_ID: &str = "<CLIENT_ID>"; // Only required for certain applications. Used here as an example. static TENANT: &str = "<TENANT_ID>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +static SCOPE: &str = "User.Read"; + // The path to the public key file. static CERTIFICATE_PATH: &str = "<CERTIFICATE_PATH>"; @@ -56,12 +60,11 @@ pub struct AccessCode { code: String, } -pub fn authorization_sign_in(client_id: &str, tenant_id: &str) { - let url = AuthorizationCodeCertificateCredential::authorization_url_builder() - .with_client_id(client_id) - .with_tenant(tenant_id) - .with_redirect_uri("http://localhost:8080") - .with_scope(vec!["User.Read"]) +pub fn authorization_sign_in() { + let url = AuthorizationCodeCertificateCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -105,8 +108,8 @@ async fn handle_redirect( .with_authorization_code_x509_certificate(authorization_code, &x509) .unwrap() .with_tenant(TENANT) - .with_scope(vec!["User.Read"]) - .with_redirect_uri("http://localhost:8080") + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) .unwrap() .build(); @@ -155,7 +158,7 @@ pub async fn start_server_main() { .and(query) .and_then(handle_redirect); - authorization_sign_in(CLIENT_ID, TENANT); + authorization_sign_in(); warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; } diff --git a/examples/users/todos/tasks.rs b/examples/users/todos/tasks.rs index 5f4dead6..2a564a91 100644 --- a/examples/users/todos/tasks.rs +++ b/examples/users/todos/tasks.rs @@ -1,5 +1,5 @@ use futures::StreamExt; -use graph_error::GraphResult; +use graph_rs_sdk::error::GraphResult; use graph_rs_sdk::Graph; use std::collections::VecDeque; @@ -32,7 +32,7 @@ async fn list_tasks(user_id: &str, list_id: &str) -> GraphResult<()> { .tasks() .list_tasks() .paging() - .stream::<TodoListTaskCollection>(); + .stream::<TodoListTaskCollection>()?; while let Some(result) = stream.next().await { let response = result?; diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 4e0c1bd1..531434b8 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,4 +1,5 @@ use crate::AuthorizationResult; +use tokio::sync::mpsc::error::SendTimeoutError; pub type AF = AuthorizationFailure; @@ -92,4 +93,16 @@ pub enum AuthExecutionError { RequestError(#[from] reqwest::Error), #[error("{0:#?}")] SerdeError(#[from] serde_json::error::Error), + #[error("{0:#?}")] + HttpError(#[from] http::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum AuthTaskExecutionError<R> { + #[error("{0:#?}")] + AuthExecutionError(#[from] AuthExecutionError), + #[error("Tokio SendTimeoutError - Reason: {0:#?}")] + SendTimeoutErrorAsync(#[from] SendTimeoutError<R>), + #[error("{0:#?}")] + JoinError(#[from] tokio::task::JoinError), } diff --git a/graph-error/src/error.rs b/graph-error/src/error.rs index 633a5804..d35c0ed0 100644 --- a/graph-error/src/error.rs +++ b/graph-error/src/error.rs @@ -35,6 +35,16 @@ pub struct ErrorStatus { pub inner_error: Option<InnerError>, } +#[derive(thiserror::Error, Debug)] +pub enum HttpResponseErrorMessage { + #[error("{0:#?}")] + GraphErrorMessage(#[from] ErrorMessage), + #[error("{0:#?}")] + SerdeJsonError(#[from] serde_json::error::Error), + #[error("{0:#?}")] + ReqwestError(#[from] reqwest::Error), +} + #[derive(thiserror::Error, Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct ErrorMessage { pub error: ErrorStatus, diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index 2429c8d7..2d11c18d 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -18,3 +18,4 @@ pub use internal::*; pub type GraphResult<T> = Result<T, GraphFailure>; pub type AuthorizationResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; +pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; diff --git a/graph-extensions/src/http/response_converter.rs b/graph-extensions/src/http/response_converter.rs index 394c84b5..0d2adb1e 100644 --- a/graph-extensions/src/http/response_converter.rs +++ b/graph-extensions/src/http/response_converter.rs @@ -1,61 +1,79 @@ use crate::http::HttpResponseBuilderExt; use async_trait::async_trait; -use graph_error::{ErrorMessage, GraphResult}; +use graph_error::{AuthExecutionResult, ErrorMessage}; use http::Response; use serde::de::DeserializeOwned; -/* -pub async fn into_http_response_async<T: DeserializeOwned>(response: reqwest::Response) -> GraphResult<http::Response<Result<T, ErrorMessage>>> { - let status = response.status(); - let url = response.url().clone(); - let headers = response.headers().clone(); - let version = response.version(); +pub type JsonHttpResponse = http::Response<Result<serde_json::Value, ErrorMessage>>; - let body: serde_json::Value = response.json().await?; - let json = body.clone(); +#[async_trait] +pub trait AsyncResponseConverterExt { + async fn into_http_response_async<T: DeserializeOwned>( + self, + ) -> AuthExecutionResult<http::Response<Result<T, ErrorMessage>>>; +} - let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) - .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); +#[async_trait] +impl AsyncResponseConverterExt for reqwest::Response { + async fn into_http_response_async<T: DeserializeOwned>( + self, + ) -> AuthExecutionResult<Response<Result<T, ErrorMessage>>> { + let status = self.status(); + let url = self.url().clone(); + let headers = self.headers().clone(); + let version = self.version(); + + let body: serde_json::Value = self.json().await?; + let json = body.clone(); - let mut builder = http::Response::builder() - .url(url) - .json(&json) - .status(http::StatusCode::from(&status)) - .version(version); + let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) + .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); - Ok(builder.body(body_result)?) + let mut builder = http::Response::builder() + .url(url) + .json(&json) + .status(http::StatusCode::from(&status)) + .version(version); + + for builder_header in builder.headers_mut().iter_mut() { + builder_header.extend(headers.clone()); + } + + Ok(builder.body(body_result)?) + } } - */ -#[async_trait] pub trait ResponseConverterExt { - async fn into_json_http_response_async<T: DeserializeOwned>( + fn into_http_response<T: DeserializeOwned>( self, - ) -> GraphResult<http::Response<Result<T, ErrorMessage>>>; + ) -> AuthExecutionResult<http::Response<Result<T, ErrorMessage>>>; } -#[async_trait] -impl ResponseConverterExt for reqwest::Response { - async fn into_json_http_response_async<T: DeserializeOwned>( +impl ResponseConverterExt for reqwest::blocking::Response { + fn into_http_response<T: DeserializeOwned>( self, - ) -> GraphResult<Response<Result<T, ErrorMessage>>> { + ) -> AuthExecutionResult<Response<Result<T, ErrorMessage>>> { let status = self.status(); let url = self.url().clone(); - let _headers = self.headers().clone(); + let headers = self.headers().clone(); let version = self.version(); - let body: serde_json::Value = self.json().await?; + let body: serde_json::Value = self.json()?; let json = body.clone(); let body_result: Result<T, ErrorMessage> = serde_json::from_value(body) .map_err(|_| serde_json::from_value(json.clone()).unwrap_or(ErrorMessage::default())); - let builder = http::Response::builder() + let mut builder = http::Response::builder() .url(url) .json(&json) .status(http::StatusCode::from(&status)) .version(version); + for builder_header in builder.headers_mut().iter_mut() { + builder_header.extend(headers.clone()); + } + Ok(builder.body(body_result)?) } } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index eab8a5e4..c37c5395 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -24,6 +24,7 @@ pub struct AppConfig { /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. pub(crate) redirect_uri: Option<Url>, + pub(crate) token_store: HashMap<String, String>, } impl AppConfig { @@ -36,6 +37,7 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + token_store: Default::default(), } } @@ -48,6 +50,7 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + token_store: Default::default(), } } @@ -63,6 +66,7 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + token_store: Default::default(), } } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index ac16211b..5172ab07 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -4,8 +4,8 @@ use crate::identity::{ application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, - DeviceCodeCredentialBuilder, EnvironmentCredential, OpenIdCredentialBuilder, - PublicClientApplication, + DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, + OpenIdCredentialBuilder, PublicClientApplication, }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; @@ -231,6 +231,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + token_store: Default::default(), }, }) } @@ -306,8 +307,8 @@ impl PublicClientApplicationBuilder { self } - pub fn with_device_code_builder(self) -> DeviceCodeCredentialBuilder { - DeviceCodeCredentialBuilder::new_with_app_config(self.app_config) + pub fn with_device_code_authorization_executor(self) -> DeviceCodePollingExecutor { + DeviceCodePollingExecutor::new_with_app_config(self.app_config) } pub fn with_device_code(self, device_code: impl AsRef<str>) -> DeviceCodeCredentialBuilder { @@ -320,7 +321,8 @@ impl PublicClientApplicationBuilder { } */ - pub fn try_from_environment() -> Result<PublicClientApplication, VarError> { + pub fn with_resource_owner_password_from_environment( + ) -> Result<PublicClientApplication, VarError> { EnvironmentCredential::resource_owner_password_credential() } } @@ -357,6 +359,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + token_store: Default::default(), }, }) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index 2ffbc7d7..e5bcc5e5 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -73,6 +73,7 @@ impl AuthCodeAuthorizationUrlParameters { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + token_store: Default::default(), }, response_type, response_mode: None, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index b3ff5247..017a48d7 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -78,6 +78,7 @@ impl AuthorizationCodeCertificateCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri, + token_store: Default::default(), }; Ok(AuthorizationCodeCertificateCredential { diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index f9205dc3..b6f862ee 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -94,6 +94,7 @@ impl AuthorizationCodeCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri), + token_store: Default::default(), }; Ok(AuthorizationCodeCredential { @@ -113,9 +114,10 @@ impl AuthorizationCodeCredential { pub fn builder<T: AsRef<str>, U: AsRef<str>>( client_id: T, + client_secret: T, authorization_code: U, ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new(client_id, authorization_code) + AuthorizationCodeCredentialBuilder::new(client_id, client_secret, authorization_code) } pub fn authorization_url_builder<T: AsRef<str>>( @@ -131,8 +133,9 @@ pub struct AuthorizationCodeCredentialBuilder { } impl AuthorizationCodeCredentialBuilder { - pub fn new<T: AsRef<str>, U: AsRef<str>>( + fn new<T: AsRef<str>, U: AsRef<str>>( client_id: T, + client_secret: T, authorization_code: U, ) -> AuthorizationCodeCredentialBuilder { Self { @@ -140,7 +143,7 @@ impl AuthorizationCodeCredentialBuilder { app_config: AppConfig::new_with_client_id(client_id.as_ref()), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, - client_secret: String::new(), + client_secret: client_secret.as_ref().to_owned(), scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 68802e5c..649be919 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -31,6 +31,7 @@ impl ClientCredentialsAuthorizationUrl { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri), + token_store: Default::default(), }, state: None, }) diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index cdfd5b47..b69bfc25 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -187,13 +187,15 @@ mod test { #[test] fn confidential_client_new() { - let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_client_id("bb301aaa-1201-4259-a230923fds32") - .with_client_secret("CLDIE3F") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); + let credential = AuthorizationCodeCredential::builder( + Uuid::new_v4().to_string(), + "ALDSKFJLKERLKJALSDKJF2209LAKJGFL", + ) + .with_client_secret("CLDIE3F") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); let mut confidential_client = credential; let credential_uri = confidential_client @@ -209,14 +211,16 @@ mod test { #[test] fn confidential_client_tenant() { - let credential = AuthorizationCodeCredential::builder("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_client_id("bb301aaa-1201-4259-a230923fds32") - .with_client_secret("CLDIE3F") - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .with_authority(Authority::Consumers) - .build(); - let mut confidential_client = credential; + let mut confidential_client = AuthorizationCodeCredential::builder( + Uuid::new_v4().to_string(), + "ALDSKFJLKERLKJALSDKJF2209LAKJGFL", + ) + .with_client_id("bb301aaa-1201-4259-a230923fds32") + .with_client_secret("CLDIE3F") + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .with_authority(Authority::Consumers) + .build(); let credential_uri = confidential_client .credential .uri(&AzureCloudInstance::AzurePublic) diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index e686f7e0..954ba0df 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -3,7 +3,8 @@ use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; use graph_error::{ - AuthExecutionError, AuthExecutionResult, AuthorizationFailure, AuthorizationResult, AF, + AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, + AuthorizationResult, AF, }; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; @@ -13,6 +14,9 @@ use std::time::Duration; use crate::identity::credentials::app_config::AppConfig; +use graph_extensions::http::{ + AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, +}; use url::Url; use uuid::Uuid; @@ -73,101 +77,6 @@ impl DeviceCodeCredential { self } - pub async fn poll_async( - &mut self, - buffer: Option<usize>, - ) -> AuthExecutionResult<tokio::sync::mpsc::Receiver<http::Response<serde_json::Value>>> { - let (sender, receiver) = { - if let Some(buffer) = buffer { - tokio::sync::mpsc::channel(buffer) - } else { - tokio::sync::mpsc::channel(100) - } - }; - - let mut credential = self.clone(); - let result = credential - .execute_async() - .await - .map_err(AuthExecutionError::from); - - match result { - Ok(response) => { - let device_code_response: DeviceCode = response.json().await.unwrap(); - println!("{:#?}", device_code_response); - - let device_code = device_code_response.device_code; - let interval = device_code_response.interval; - let _message = device_code_response.message; - credential.with_device_code(device_code); - - tokio::spawn(async move { - let mut should_slow_down = false; - - loop { - // Wait the amount of seconds that interval is. - if should_slow_down { - std::thread::sleep(interval.add(Duration::from_secs(5))); - } else { - std::thread::sleep(interval); - } - - let response = credential.execute_async().await.unwrap(); - - let status = response.status(); - println!("{response:#?}"); - - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); - - if status.is_success() { - sender - .send_timeout( - http::Response::builder().status(status).body(body).unwrap(), - Duration::from_secs(60), - ) - .await - .unwrap(); - } else { - let option_error = body["error"].as_str().map(|value| value.to_owned()); - sender - .send_timeout( - http::Response::builder().status(status).body(body).unwrap(), - Duration::from_secs(60), - ) - .await - .unwrap(); - - if let Some(error) = option_error { - match PollDeviceCodeType::from_str(error.as_str()) { - Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeType::AuthorizationPending => continue, - PollDeviceCodeType::AuthorizationDeclined => break, - PollDeviceCodeType::BadVerificationCode => continue, - PollDeviceCodeType::ExpiredToken => break, - PollDeviceCodeType::InvalidType => break, - PollDeviceCodeType::AccessDenied => break, - PollDeviceCodeType::SlowDown => { - should_slow_down = true; - continue; - } - }, - Err(_) => break, - } - } else { - // Body should have error or we should bail. - break; - } - } - } - }); - } - Err(err) => return Err(err), - } - - Ok(receiver) - } - pub fn builder(client_id: impl AsRef<str>) -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new(client_id.as_ref()) } @@ -207,7 +116,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { if refresh_token.trim().is_empty() { return AuthorizationFailure::msg_result( OAuthParameter::RefreshToken.alias(), - "Refresh token string is empty - Either device code or refresh token is required", + "Found empty string for refresh token", ); } @@ -228,7 +137,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { if device_code.trim().is_empty() { return AuthorizationFailure::msg_result( OAuthParameter::DeviceCode.alias(), - "Either device code or refresh token is required - found empty device code", + "Found empty string for device code", ); } @@ -288,18 +197,6 @@ impl DeviceCodeCredentialBuilder { } } - pub(crate) fn new_with_app_config(app_config: AppConfig) -> DeviceCodeCredentialBuilder { - DeviceCodeCredentialBuilder { - credential: DeviceCodeCredential { - app_config, - refresh_token: None, - device_code: None, - scope: vec![], - serializer: Default::default(), - }, - } - } - pub(crate) fn new_with_device_code<T: AsRef<str>>( device_code: T, app_config: AppConfig, @@ -348,6 +245,183 @@ impl From<&DeviceCode> for DeviceCodeCredentialBuilder { } } +pub struct DeviceCodePollingExecutor { + credential: DeviceCodeCredential, +} + +impl DeviceCodePollingExecutor { + pub(crate) fn new_with_app_config(app_config: AppConfig) -> DeviceCodePollingExecutor { + DeviceCodePollingExecutor { + credential: DeviceCodeCredential { + app_config, + refresh_token: None, + device_code: None, + scope: vec![], + serializer: Default::default(), + }, + } + } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn poll(&mut self) -> AuthExecutionResult<std::sync::mpsc::Receiver<JsonHttpResponse>> { + let (sender, receiver) = std::sync::mpsc::channel(); + + let mut credential = self.credential.clone(); + let response = credential.execute()?; + + let http_response = response.into_http_response()?; + let json = http_response.json().unwrap(); + let device_code_response: DeviceCode = serde_json::from_value(json)?; + + sender.send(http_response).unwrap(); + + let device_code = device_code_response.device_code; + let interval = Duration::from_secs(device_code_response.interval); + credential.with_device_code(device_code); + + let _ = std::thread::spawn(move || { + let mut should_slow_down = false; + + loop { + // Wait the amount of seconds that interval is. + if should_slow_down { + should_slow_down = false; + std::thread::sleep(interval.add(Duration::from_secs(5))); + } else { + std::thread::sleep(interval); + } + + let response = credential.execute().unwrap(); + let http_response = response.into_http_response()?; + let status = http_response.status(); + + if status.is_success() { + sender.send(http_response)?; + break; + } else { + let json = http_response.json().unwrap(); + let option_error = json["error"].as_str().map(|value| value.to_owned()); + sender.send(http_response)?; + + if let Some(error) = option_error { + match PollDeviceCodeType::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeType::AuthorizationPending => continue, + PollDeviceCodeType::AuthorizationDeclined => break, + PollDeviceCodeType::BadVerificationCode => continue, + PollDeviceCodeType::ExpiredToken => break, + PollDeviceCodeType::AccessDenied => break, + PollDeviceCodeType::SlowDown => { + should_slow_down = true; + continue; + } + }, + Err(_) => break, + } + } else { + // Body should have error or we should bail. + break; + } + } + } + Ok::<(), anyhow::Error>(()) + }); + + Ok(receiver) + } + + pub async fn poll_async( + &mut self, + buffer: Option<usize>, + ) -> AuthTaskExecutionResult<tokio::sync::mpsc::Receiver<JsonHttpResponse>, JsonHttpResponse> + { + let (sender, receiver) = { + if let Some(buffer) = buffer { + tokio::sync::mpsc::channel(buffer) + } else { + tokio::sync::mpsc::channel(100) + } + }; + + let mut credential = self.credential.clone(); + let response = credential.execute_async().await?; + + let http_response = response.into_http_response_async().await?; + let json = http_response.json().unwrap(); + let device_code_response: DeviceCode = + serde_json::from_value(json).map_err(AuthExecutionError::from)?; + + sender + .send_timeout(http_response, Duration::from_secs(60)) + .await?; + + let device_code = device_code_response.device_code; + let mut interval = Duration::from_secs(device_code_response.interval); + credential.with_device_code(device_code); + + let _ = tokio::spawn(async move { + let mut should_slow_down = false; + + loop { + // Should slow down is part of the openid connect spec and means that + // that we should wait longer between polling by the amount specified + // in the interval field of the device code. + if should_slow_down { + should_slow_down = false; + interval = interval.add(Duration::from_secs(5)); + } + + // Wait the amount of seconds that interval is. + tokio::time::sleep(interval).await; + + let response = credential.execute_async().await?; + let http_response = response.into_http_response_async().await?; + let status = http_response.status(); + + if status.is_success() { + sender + .send_timeout(http_response, Duration::from_secs(60)) + .await?; + break; + } else { + let json = http_response.json().unwrap(); + let option_error = json["error"].as_str().map(|value| value.to_owned()); + sender + .send_timeout(http_response, Duration::from_secs(60)) + .await?; + + if let Some(error) = option_error { + match PollDeviceCodeType::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeType::AuthorizationPending => continue, + PollDeviceCodeType::AuthorizationDeclined => break, + PollDeviceCodeType::BadVerificationCode => continue, + PollDeviceCodeType::ExpiredToken => break, + PollDeviceCodeType::AccessDenied => break, + PollDeviceCodeType::SlowDown => { + should_slow_down = true; + continue; + } + }, + Err(_) => break, + } + } else { + // Body should have error or we should bail. + break; + } + } + } + return Ok::<(), anyhow::Error>(()); + }); + + Ok(receiver) + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 4a918260..abae75bd 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -41,7 +41,7 @@ pub struct OpenIdAuthorizationUrl { /// - form_post: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub(crate) response_mode: Option<ResponseMode>, - /// Optional + /// Required /// A value generated and sent by your app in its request for an ID token. The same nonce /// value is included in the ID token returned to your app by the Microsoft identity platform. /// To mitigate token replay attacks, your app should verify the nonce value in the ID token @@ -116,6 +116,7 @@ impl OpenIdAuthorizationUrl { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + token_store: Default::default(), }, response_type: BTreeSet::new(), response_mode: None, @@ -414,19 +415,6 @@ impl OpenIdAuthorizationUrlBuilder { self } - /// Automatically adds profile, email, and offline_access to the scope parameter. - /// The openid scope is already included when using [OpenIdCredential] - pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { - self.with_nonce_generated()?; - self.with_response_type(vec![ResponseType::Code, ResponseType::IdToken]); - self.auth_url_parameters.scope.extend( - vec!["profile", "email", "offline_access"] - .into_iter() - .map(|s| s.to_string()), - ); - Ok(self) - } - /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 401b195f..9eae8a77 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -76,6 +76,7 @@ impl OpenIdCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), + token_store: Default::default(), }, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 18a3813d..69e267cc 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -11,6 +11,10 @@ use std::collections::HashMap; use url::Url; use uuid::Uuid; +pub struct UserInfoEndpoint { + user_info_endpoint: String, +} + #[async_trait] pub trait TokenCredentialExecutor { fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url>; @@ -28,7 +32,7 @@ pub trait TokenCredentialExecutor { fn openid_configuration_url(&self) -> AuthorizationResult<Url> { Ok(Url::parse( format!( - "{}/{}/2.0/.well-known/openid-configuration", + "{}/{}/v2.0/.well-known/openid-configuration", self.azure_cloud_instance().as_ref(), self.authority().as_ref() ) @@ -36,6 +40,26 @@ pub trait TokenCredentialExecutor { )?) } + fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { + let response = self.get_openid_config()?; + let config: serde_json::Value = response.json()?; + let user_info_endpoint = Url::parse(config["userinfo_endpoint"].as_str().unwrap()).unwrap(); + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + let response = http_client + .get(user_info_endpoint) + .headers(headers) + .send() + .expect("Error on header"); + + Ok(response) + } + fn get_openid_config(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let open_id_url = self.openid_configuration_url()?; let http_client = reqwest::blocking::ClientBuilder::new() @@ -149,7 +173,7 @@ mod test { let url = open_id.openid_configuration_url().unwrap(); assert_eq!( - "https://login.microsoftonline.com/tenant-id/2.0/.well-known/openid-configuration", + "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration", url.as_str() ) } @@ -162,7 +186,7 @@ mod test { let url = open_id.openid_configuration_url().unwrap(); assert_eq!( - "https://login.microsoftonline.com/common/2.0/.well-known/openid-configuration", + "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", url.as_str() ) } diff --git a/graph-oauth/src/identity/device_code.rs b/graph-oauth/src/identity/device_code.rs index 2eee6f92..b777ee8e 100644 --- a/graph-oauth/src/identity/device_code.rs +++ b/graph-oauth/src/identity/device_code.rs @@ -1,7 +1,6 @@ use serde_json::Value; use std::collections::{BTreeSet, HashMap}; use std::str::FromStr; -use std::time::Duration; /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 /// The actual device code response that is received from Microsoft Graph @@ -29,7 +28,7 @@ pub struct DeviceCode { /// SHOULD wait between polling requests to the token endpoint. If no /// value is provided, clients MUST use 5 as the default. #[serde(default = "default_interval")] - pub interval: Duration, + pub interval: u64, /// User friendly text response that can be used for display purpose. pub message: String, pub user_code: String, @@ -46,8 +45,8 @@ pub struct DeviceCode { pub additional_fields: HashMap<String, Value>, } -fn default_interval() -> Duration { - Duration::from_secs(5) +fn default_interval() -> u64 { + 5 } /// Response types used when polling for a device code @@ -79,13 +78,6 @@ pub enum PollDeviceCodeType { /// still pending and polling should continue, but the interval MUST /// be increased by 5 seconds for this and all subsequent requests. SlowDown, - - /// Indicates the value is not an actual PollDeviceCodeType - this is an internal type not a - /// type used in Microsoft Identity Platform or in the OAuth2/OpenId specification. - /// - /// This is a catch all to prevent parsing errors and break from - /// any loop that is used to poll for the device code. - InvalidType, } impl FromStr for PollDeviceCodeType { @@ -99,7 +91,7 @@ impl FromStr for PollDeviceCodeType { "expired_token" => Ok(PollDeviceCodeType::ExpiredToken), "access_denied" => Ok(PollDeviceCodeType::AccessDenied), "slow_down" => Ok(PollDeviceCodeType::SlowDown), - _ => Ok(PollDeviceCodeType::InvalidType), + _ => Err(()), } } } From f1d0a8f2b4d6508fe50999fdb154e52ba132e7c6 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 26 Sep 2023 08:43:27 -0400 Subject: [PATCH 040/118] Start implementing client applications into graph-http and the main client --- examples/oauth/README.md | 15 +- .../auth_code_grant/auth_code_grant_pkce.rs | 7 +- .../auth_code_grant/auth_code_grant_secret.rs | 4 +- examples/oauth/client_credentials/mod.rs | 8 +- examples/oauth/device_code.rs | 6 +- examples/oauth/is_access_token_expired.rs | 10 +- examples/oauth/main.rs | 24 +- .../openid_connect_form_post.rs | 6 +- examples/oauth_authorization_url/main.rs | 2 +- examples/oauth_certificate/main.rs | 4 +- graph-extensions/Cargo.toml | 10 + .../src/cache/in_memory_credential_store.rs | 54 +++ graph-extensions/src/cache/mod.rs | 45 +++ graph-extensions/src/cache/token_store.rs | 71 ++++ .../src/cache/token_store_providers.rs | 4 +- graph-extensions/src/lib.rs | 9 + .../src/token/client_application.rs | 23 ++ .../src/token}/id_token.rs | 34 +- graph-extensions/src/token/mod.rs | 7 + .../src/token/msal_token.rs | 305 +++++++++------ .../src/web/interactive_authenticator.rs | 7 +- .../src/web/interactive_web_view.rs | 4 +- .../src/web/mod.rs | 4 +- .../src/web/web_view_options.rs | 6 +- graph-http/Cargo.toml | 2 + graph-http/src/client.rs | 143 ++++++- graph-http/src/request_handler.rs | 5 +- graph-oauth/Cargo.toml | 1 + graph-oauth/src/auth.rs | 38 +- .../in_memory_credential_store.rs | 59 --- .../src/identity/credential_store/mod.rs | 45 --- .../src/identity/credentials/app_config.rs | 13 +- .../credentials/application_builder.rs | 24 +- .../auth_code_authorization_url_parameters.rs | 9 +- ...thorization_code_certificate_credential.rs | 14 +- .../authorization_code_credential.rs | 50 ++- .../credentials/client_application.rs | 62 --- .../client_assertion_credential.rs | 5 +- .../client_certificate_credential.rs | 5 +- .../client_credentials_authorization_url.rs | 1 - .../credentials/client_secret_credential.rs | 5 +- .../confidential_client_application.rs | 357 +++++++++++++----- .../credentials/device_code_credential.rs | 5 +- .../credentials/environment_credential.rs | 7 +- graph-oauth/src/identity/credentials/mod.rs | 2 - .../credentials/open_id_authorization_url.rs | 1 - .../credentials/open_id_credential.rs | 6 +- .../credentials/public_client_application.rs | 127 ++++++- .../resource_owner_password_credential.rs | 21 +- .../credentials/token_credential_executor.rs | 122 ++++-- graph-oauth/src/identity/mod.rs | 4 +- graph-oauth/src/identity/token_validator.rs | 15 + graph-oauth/src/lib.rs | 16 +- src/client/graph.rs | 25 +- test-tools/src/oauth_request.rs | 49 +-- tests/access_token_tests.rs | 45 --- tests/grants_authorization_code.rs | 14 +- tests/grants_code_flow.rs | 20 +- 58 files changed, 1309 insertions(+), 677 deletions(-) create mode 100644 graph-extensions/src/cache/in_memory_credential_store.rs create mode 100644 graph-extensions/src/cache/mod.rs create mode 100644 graph-extensions/src/cache/token_store.rs rename graph-oauth/src/identity/credential_store/token_cache_providers.rs => graph-extensions/src/cache/token_store_providers.rs (66%) create mode 100644 graph-extensions/src/token/client_application.rs rename {graph-oauth/src => graph-extensions/src/token}/id_token.rs (87%) create mode 100644 graph-extensions/src/token/mod.rs rename graph-oauth/src/access_token.rs => graph-extensions/src/token/msal_token.rs (53%) rename {graph-oauth => graph-extensions}/src/web/interactive_authenticator.rs (79%) rename {graph-oauth => graph-extensions}/src/web/interactive_web_view.rs (98%) rename {graph-oauth => graph-extensions}/src/web/mod.rs (63%) rename graph-oauth/src/web/interactive_web_view_options.rs => graph-extensions/src/web/web_view_options.rs (84%) delete mode 100644 graph-oauth/src/identity/credential_store/in_memory_credential_store.rs delete mode 100644 graph-oauth/src/identity/credential_store/mod.rs delete mode 100644 graph-oauth/src/identity/credentials/client_application.rs create mode 100644 graph-oauth/src/identity/token_validator.rs delete mode 100644 tests/access_token_tests.rs diff --git a/examples/oauth/README.md b/examples/oauth/README.md index c2fac63a..e3e7dc52 100644 --- a/examples/oauth/README.md +++ b/examples/oauth/README.md @@ -1,8 +1,16 @@ # OAuth Overview -### Authorization Code Grant +There are two main types for building your chosen OAuth or OpenId Connect Flow. -Getting the Confidential Client +- `PublicClientApplication` +- `ConfidentialClientApplication` + + +### Authorization Code Grant + +The authorization code grant is considered a confidential client (except in the hybrid flow) +and we can get an access token by using the authorization code returned in the query of the URL +on redirect after authorization sign in is performed by the user. ```rust use graph_rs_sdk::oauth::{ @@ -14,9 +22,10 @@ fn main() { let client_id = "<CLIENT_ID>"; let client_secret = "<CLIENT_SECRET>"; let scope = vec!["<SCOPE>", "<SCOPE>"]; + let redirect_uri = "http://localhost:8080"; let mut confidential_client = ConfidentialClientApplication::builder(client_id) - .with_authorization_code(authorization_code) + .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) .with_scope(SCOPE.clone()) .with_redirect_uri(REDIRECT_URI) diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index de5c4d84..061cd9f4 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -2,7 +2,7 @@ use graph_oauth::identity::ResponseType; use graph_rs_sdk::error::AuthorizationResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalTokenResponse, ProofKeyForCodeExchange, TokenCredentialExecutor, TokenRequest, + MsalToken, ProofKeyForCodeExchange, TokenCredentialExecutor, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; @@ -57,8 +57,7 @@ async fn handle_redirect( let authorization_code = access_code.code; let mut confidential_client = - AuthorizationCodeCredential::builder(CLIENT_ID, authorization_code) - .with_client_secret(CLIENT_SECRET) + AuthorizationCodeCredential::builder(CLIENT_ID, CLIENT_SECRET, authorization_code) .with_redirect_uri("http://localhost:8000/redirect") .unwrap() .with_pkce(&PKCE) @@ -69,7 +68,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let access_token: MsalTokenResponse = response.json().await.unwrap(); + let access_token: MsalToken = response.json().await.unwrap(); // If all went well here we can print out the OAuth config with the Access Token. println!("AccessToken: {:#?}", access_token.access_token); diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index cba98cea..804c61c1 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::ErrorMessage; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalTokenResponse, TokenCredentialExecutor, TokenRequest, + MsalToken, TokenCredentialExecutor, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; @@ -78,7 +78,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let mut access_token: MsalTokenResponse = response.json().await.unwrap(); + let mut access_token: MsalToken = response.json().await.unwrap(); // Enables the printing of the bearer, refresh, and id token. access_token.enable_pii_logging(true); diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index e0868cd7..aa65718a 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -10,8 +10,8 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, - TokenCredentialExecutor, TokenRequest, + ClientSecretCredential, ConfidentialClientApplication, MsalToken, TokenCredentialExecutor, + TokenRequest, }; mod client_credentials_admin_consent; @@ -37,7 +37,7 @@ pub async fn get_token_silent() { .unwrap(); println!("{response:#?}"); - let body: MsalTokenResponse = response.json().await.unwrap(); + let body: MsalToken = response.json().await.unwrap(); } pub async fn get_token_silent2() { @@ -48,5 +48,5 @@ pub async fn get_token_silent2() { let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let body: MsalTokenResponse = response.json().await.unwrap(); + let body: MsalToken = response.json().await.unwrap(); } diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 8662b71d..17cef1a7 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -2,7 +2,7 @@ use graph_oauth::identity::{ DeviceCodeCredential, PublicClientApplication, TokenCredentialExecutor, }; use graph_oauth::oauth::DeviceCodeCredentialBuilder; -use graph_rs_sdk::oauth::{MsalTokenResponse, OAuthSerializer}; +use graph_rs_sdk::oauth::{MsalToken, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; use warp::hyper::body::HttpBody; @@ -19,7 +19,7 @@ static TENANT: &str = "<TENANT>"; // has entered the and an access token is returned or an error happens. fn poll_device_code() { let mut device_executor = PublicClientApplication::builder(CLIENT_ID) - .with_device_authorization_executor() + .with_device_code_authorization_executor() .with_scope(vec!["User.Read"]) .poll() .unwrap(); @@ -39,7 +39,7 @@ fn get_token(device_code: &str) { let response = public_client.execute().unwrap(); println!("{:#?}", response); - let body: MsalTokenResponse = response.json().unwrap(); + let body: MsalToken = response.json().unwrap(); println!("{:#?}", body); } diff --git a/examples/oauth/is_access_token_expired.rs b/examples/oauth/is_access_token_expired.rs index cb4fed67..0ed35625 100644 --- a/examples/oauth/is_access_token_expired.rs +++ b/examples/oauth/is_access_token_expired.rs @@ -1,15 +1,15 @@ -use graph_rs_sdk::oauth::MsalTokenResponse; +use graph_rs_sdk::oauth::MsalToken; use std::thread; use std::time::Duration; pub fn is_access_token_expired() { - let mut access_token = MsalTokenResponse::default(); - access_token.set_expires_in(1); + let mut access_token = MsalToken::default(); + access_token.with_expires_in(1); thread::sleep(Duration::from_secs(3)); assert!(access_token.is_expired()); - let mut access_token = MsalTokenResponse::default(); - access_token.set_expires_in(10); + let mut access_token = MsalToken::default(); + access_token.with_expires_in(10); thread::sleep(Duration::from_secs(4)); assert!(!access_token.is_expired()); } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index c8c851a6..f843784b 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -23,15 +23,17 @@ mod is_access_token_expired; mod openid_connect; mod signing_keys; +use crate::is_access_token_expired::is_access_token_expired; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, MsalTokenResponse, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, MsalToken, ProofKeyForCodeExchange, PublicClientApplication, TokenCredentialExecutor, TokenRequest, }; -#[tokio::main] -async fn main() {} +fn main() { + is_access_token_expired(); +} /* // Some examples of what you can use for authentication and getting access tokens. There are @@ -57,19 +59,19 @@ async fn main() {} async fn auth_code_grant(authorization_code: &str) { let pkce = ProofKeyForCodeExchange::generate().unwrap(); - let credential = AuthorizationCodeCredential::builder("CLIENT_ID", authorization_code) - .with_client_secret("CLIENT_SECRET") - .with_redirect_uri("http://localhost:8000/redirect") - .unwrap() - .with_pkce(&pkce) - .build(); + let credential = + AuthorizationCodeCredential::builder("CLIENT_ID", "CLIENT_SECRET", authorization_code) + .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() + .with_pkce(&pkce) + .build(); let mut confidential_client = credential; let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: MsalTokenResponse = response.json().await.unwrap(); + let access_token: MsalToken = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } @@ -82,6 +84,6 @@ async fn client_credentials() { let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: MsalTokenResponse = response.json().await.unwrap(); + let access_token: MsalToken = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } diff --git a/examples/oauth/openid_connect/openid_connect_form_post.rs b/examples/oauth/openid_connect/openid_connect_form_post.rs index a34b4cbb..b3a3c566 100644 --- a/examples/oauth/openid_connect/openid_connect_form_post.rs +++ b/examples/oauth/openid_connect/openid_connect_form_post.rs @@ -3,7 +3,7 @@ use graph_oauth::identity::{ TokenRequest, }; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; -use graph_rs_sdk::oauth::{IdToken, MsalTokenResponse, OAuthSerializer}; +use graph_rs_sdk::oauth::{IdToken, MsalToken, OAuthSerializer}; use tracing_subscriber::fmt::format::FmtSpan; use url::Url; @@ -23,7 +23,7 @@ use url::Url; /// OAuth-enabled applications by using a security token called an ID token. use warp::Filter; -// Use the form post form post response mode when listening on a server instead +// Use the form post response mode when listening on a server instead // of the URL query because the the query does not get sent to servers. // The client id and client secret must be changed before running this example. @@ -65,7 +65,7 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let mut response = confidential_client.execute_async().await.unwrap(); if response.status().is_success() { - let mut access_token: MsalTokenResponse = response.json().await.unwrap(); + let mut access_token: MsalToken = response.json().await.unwrap(); access_token.enable_pii_logging(true); println!("\n{:#?}\n", access_token); diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index 863e6b2e..ccdff579 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -14,7 +14,7 @@ mod openid_connect; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, MsalTokenResponse, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, MsalToken, ProofKeyForCodeExchange, PublicClientApplication, TokenCredentialExecutor, TokenRequest, }; diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index cb346e3f..2b994e3f 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,7 +4,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AuthorizationCodeCertificateCredential, ConfidentialClientApplication, MsalTokenResponse, PKey, + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, MsalToken, PKey, TokenCredentialExecutor, X509Certificate, X509, }; use std::fs::File; @@ -118,7 +118,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let mut msal_token: MsalTokenResponse = response.json().await.unwrap(); + let mut msal_token: MsalToken = response.json().await.unwrap(); msal_token.enable_pii_logging(true); // If all went well here we can print out the Access Token. diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml index 5bac93ff..6d20333b 100644 --- a/graph-extensions/Cargo.toml +++ b/graph-extensions/Cargo.toml @@ -7,16 +7,26 @@ repository = "https://github.com/sreeise/graph-rs-sdk" description = "Extensions and utilities used across multiple crates that make up the graph-rs-sdk crate" [dependencies] +anyhow = { version = "1.0.69", features = ["backtrace"]} async-stream = "0.3" async-trait = "0.1.35" bytes = { version = "1.4.0", features = ["serde"] } +chrono = { version = "0.4.23", features = ["serde"] } +chrono-humanize = "0.2.2" +dyn-clone = "1.0.14" futures = "0.3.28" http = "0.2.9" +log = "0.4" +pretty_env_logger = "0.4" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +serde-aux = "4.1.2" serde = { version = "1", features = ["derive"] } serde_json = "1" +serde_urlencoded = "0.7.1" +time = { version = "0.3.10", features = ["local-offset", "serde"] } tokio = { version = "1.27.0", features = ["full"] } url = { version = "2", features = ["serde"] } +wry = "0.30.0" graph-error = { path = "../graph-error" } diff --git a/graph-extensions/src/cache/in_memory_credential_store.rs b/graph-extensions/src/cache/in_memory_credential_store.rs new file mode 100644 index 00000000..b8470752 --- /dev/null +++ b/graph-extensions/src/cache/in_memory_credential_store.rs @@ -0,0 +1,54 @@ +use crate::cache::{StoredToken, TokenStore, TokenStoreProvider}; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct InMemoryCredentialStore { + store: HashMap<String, StoredToken>, +} + +impl InMemoryCredentialStore { + pub fn new(id: String, stored_token: StoredToken) -> InMemoryCredentialStore { + let mut store = HashMap::new(); + store.insert(id, stored_token); + + InMemoryCredentialStore { store } + } +} + +impl TokenStore for InMemoryCredentialStore { + fn token_store_provider(&self) -> TokenStoreProvider { + TokenStoreProvider::InMemory + } + + fn is_stored_token_initialized(&self, id: &str) -> bool { + if let Some(stored_token) = self.store.get(id) { + stored_token.is_initialized() + } else { + false + } + } + + fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { + self.store.get(id) + } + + fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { + self.store.insert(id.to_string(), stored_token) + } + + fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { + if let Some(stored_token) = self.store.get(id) { + stored_token.get_bearer_token() + } else { + None + } + } + + fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { + if let Some(stored_token) = self.store.get(id) { + stored_token.get_refresh_token() + } else { + None + } + } +} diff --git a/graph-extensions/src/cache/mod.rs b/graph-extensions/src/cache/mod.rs new file mode 100644 index 00000000..f3d21c4d --- /dev/null +++ b/graph-extensions/src/cache/mod.rs @@ -0,0 +1,45 @@ +mod in_memory_credential_store; +mod token_store; +mod token_store_providers; + +pub use in_memory_credential_store::*; +use std::fmt::{Debug, Formatter}; +pub use token_store::*; +pub use token_store_providers::*; + +#[derive(Clone)] +pub struct UnInitializedTokenStore; + +impl TokenStore for UnInitializedTokenStore { + fn token_store_provider(&self) -> TokenStoreProvider { + TokenStoreProvider::UnInitialized + } + + fn is_stored_token_initialized(&self, _id: &str) -> bool { + false + } + + fn get_stored_token(&self, _id: &str) -> Option<&StoredToken> { + panic!("UnInitializedTokenStore does not store tokens") + } + + fn update_stored_token(&mut self, _id: &str, stored_token: StoredToken) -> Option<StoredToken> { + panic!("UnInitializedTokenStore does not store tokens") + } + + fn get_bearer_token_from_store(&self, _id: &str) -> Option<&String> { + info!("Using uninitialized token store - empty string returned for bearer token"); + Default::default() + } + + fn get_refresh_token_from_store(&self, _id: &str) -> Option<&String> { + info!("Using uninitialized token store - None returned for refresh token"); + Default::default() + } +} + +impl Debug for UnInitializedTokenStore { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("UnInitializedTokenStore") + } +} diff --git a/graph-extensions/src/cache/token_store.rs b/graph-extensions/src/cache/token_store.rs new file mode 100644 index 00000000..934b058e --- /dev/null +++ b/graph-extensions/src/cache/token_store.rs @@ -0,0 +1,71 @@ +use crate::cache::TokenStoreProvider; +use crate::token::MsalToken; +use dyn_clone::DynClone; + +#[derive(Debug, Clone, Eq, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum StoredToken { + BearerToken(String), + MsalToken(MsalToken), + BearerAndRefreshToken { bearer: String, refresh: String }, + UnInitialized, +} + +impl StoredToken { + pub fn is_initialized(&self) -> bool { + !self.eq(&StoredToken::UnInitialized) + } + + pub fn enable_pii_logging(&mut self) { + match self { + StoredToken::MsalToken(token) => { + token.enable_pii_logging(true); + } + _ => {} + } + } + + pub fn get_bearer_token(&self) -> Option<&String> { + match self { + StoredToken::BearerToken(bearer) => Some(bearer), + StoredToken::MsalToken(msal_token) => Some(&msal_token.access_token), + StoredToken::BearerAndRefreshToken { bearer, refresh: _ } => Some(bearer), + StoredToken::UnInitialized => None, + } + } + + pub fn get_refresh_token(&self) -> Option<&String> { + match self { + StoredToken::BearerToken(_) => None, + StoredToken::MsalToken(msal_token) => msal_token.refresh_token.as_ref(), + StoredToken::BearerAndRefreshToken { bearer: _, refresh } => Some(refresh), + StoredToken::UnInitialized => None, + } + } +} + +dyn_clone::clone_trait_object!(TokenStore); + +pub trait TokenStore: DynClone { + fn token_store_provider(&self) -> TokenStoreProvider; + + fn is_token_store_initialized(&self) -> bool { + !self + .token_store_provider() + .eq(&TokenStoreProvider::UnInitialized) + } + + fn is_stored_token_initialized(&self, id: &str) -> bool; + + fn is_store_and_token_initialized(&self, id: &str) -> bool { + self.is_token_store_initialized() && self.is_stored_token_initialized(id) + } + + fn get_stored_token(&self, id: &str) -> Option<&StoredToken>; + + fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken>; + + fn get_bearer_token_from_store(&self, id: &str) -> Option<&String>; + + fn get_refresh_token_from_store(&self, id: &str) -> Option<&String>; +} diff --git a/graph-oauth/src/identity/credential_store/token_cache_providers.rs b/graph-extensions/src/cache/token_store_providers.rs similarity index 66% rename from graph-oauth/src/identity/credential_store/token_cache_providers.rs rename to graph-extensions/src/cache/token_store_providers.rs index 572097d2..43a4f70d 100644 --- a/graph-oauth/src/identity/credential_store/token_cache_providers.rs +++ b/graph-extensions/src/cache/token_store_providers.rs @@ -1,7 +1,5 @@ #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum TokenCacheProviderType { +pub enum TokenStoreProvider { UnInitialized, InMemory, - Session, - Distributed, } diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs index 3883215f..373bd7dd 100644 --- a/graph-extensions/src/lib.rs +++ b/graph-extensions/src/lib.rs @@ -1 +1,10 @@ +#[macro_use] +extern crate serde; +#[macro_use] +extern crate log; +extern crate pretty_env_logger; + +pub mod cache; pub mod http; +pub mod token; +pub mod web; diff --git a/graph-extensions/src/token/client_application.rs b/graph-extensions/src/token/client_application.rs new file mode 100644 index 00000000..3980b94c --- /dev/null +++ b/graph-extensions/src/token/client_application.rs @@ -0,0 +1,23 @@ +use crate::cache::{StoredToken, TokenStore}; +use async_trait::async_trait; +use dyn_clone::DynClone; +use graph_error::AuthExecutionResult; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum ClientApplicationType { + ConfidentialClientApplication, + PublicClientApplication, +} + +dyn_clone::clone_trait_object!(ClientApplication); + +#[async_trait] +pub trait ClientApplication: TokenStore + DynClone { + fn client_application_type(&self) -> ClientApplicationType; + + fn get_token_silent(&mut self) -> AuthExecutionResult<String>; + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; + + fn get_stored_application_token(&mut self) -> Option<&StoredToken>; +} diff --git a/graph-oauth/src/id_token.rs b/graph-extensions/src/token/id_token.rs similarity index 87% rename from graph-oauth/src/id_token.rs rename to graph-extensions/src/token/id_token.rs index 6db4c300..c0dd313b 100644 --- a/graph-oauth/src/id_token.rs +++ b/graph-extensions/src/token/id_token.rs @@ -1,4 +1,4 @@ -use crate::jwt::{JsonWebToken, JwtParser}; +//use crate::jwt::{JsonWebToken, JwtParser}; use serde::de::{Error, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; @@ -9,6 +9,10 @@ use std::fmt::{Debug, Formatter}; use std::str::FromStr; use url::form_urlencoded::parse; +/// ID tokens are sent to the client application as part of an OpenID Connect flow. +/// They can be sent alongside or instead of an access token. ID tokens are used by the +/// client to authenticate the user. To learn more about how the Microsoft identity +/// platform issues ID tokens, see [ID tokens in the Microsoft identity platform.](https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens) #[derive(Default, Clone, Eq, PartialEq, Serialize)] pub struct IdToken { pub code: Option<String>, @@ -37,9 +41,9 @@ impl IdToken { self.id_token = id_token.into(); } - pub fn jwt(&self) -> Option<JsonWebToken> { + /*pub fn jwt(&self) -> Option<JsonWebToken> { JwtParser::parse(self.id_token.as_str()).ok() - } + }*/ pub fn code(&mut self, code: &str) { self.code = Some(code.into()); @@ -60,22 +64,6 @@ impl IdToken { pub fn enable_pii_logging(&mut self, log_pii: bool) { self.log_pii = log_pii; } - - pub fn get_id_token(&self) -> String { - self.id_token.clone() - } - - pub fn get_code(&self) -> Option<String> { - self.code.clone() - } - - pub fn get_state(&self) -> Option<String> { - self.state.clone() - } - - pub fn get_session_state(&self) -> Option<String> { - self.session_state.clone() - } } impl TryFrom<String> for IdToken { @@ -155,10 +143,10 @@ impl<'de> Deserialize<'de> for IdToken { let mut id_token = IdToken::default(); while let Ok(Some((key, value))) = map.next_entry::<String, String>() { match key.as_bytes() { - b"code" => id_token.code(value.as_ref()), - b"id_token" => id_token.id_token(value.as_ref()), - b"state" => id_token.state(value.as_ref()), - b"session_state" => id_token.session_state(value.as_ref()), + b"code" => id_token.code = Some(value), + b"id_token" => id_token.id_token = value, + b"state" => id_token.state = Some(value), + b"session_state" => id_token.session_state = Some(value), _ => { id_token .additional_fields diff --git a/graph-extensions/src/token/mod.rs b/graph-extensions/src/token/mod.rs new file mode 100644 index 00000000..92c53506 --- /dev/null +++ b/graph-extensions/src/token/mod.rs @@ -0,0 +1,7 @@ +mod client_application; +mod id_token; +mod msal_token; + +pub use client_application::*; +pub use id_token::*; +pub use msal_token::*; diff --git a/graph-oauth/src/access_token.rs b/graph-extensions/src/token/msal_token.rs similarity index 53% rename from graph-oauth/src/access_token.rs rename to graph-extensions/src/token/msal_token.rs index b1519f12..02ce2ae1 100644 --- a/graph-oauth/src/access_token.rs +++ b/graph-extensions/src/token/msal_token.rs @@ -1,5 +1,3 @@ -use crate::id_token::IdToken; -use crate::jwt::{JsonWebToken, JwtParser}; use chrono::{DateTime, Duration, Utc}; use chrono_humanize::HumanTime; use graph_error::GraphFailure; @@ -8,20 +6,37 @@ use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; +use std::ops::{Add, AddAssign}; +use crate::token::IdToken; use std::str::FromStr; +use time::OffsetDateTime; + +fn deserialize_scope<'de, D>(scope: D) -> Result<Vec<String>, D::Error> +where + D: Deserializer<'de>, +{ + let scope_string: Result<String, D::Error> = serde::Deserialize::deserialize(scope); + if let Ok(scope) = scope_string { + Ok(scope.split(' ').map(|scope| scope.to_owned()).collect()) + } else { + Ok(vec![]) + } +} // Used to set timestamp based on expires in // which can only be done after deserialization. #[derive(Clone, Serialize, Deserialize)] -struct PhantomAccessToken { +struct PhantomMsalToken { access_token: String, token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] expires_in: i64, /// Legacy version of expires_in ext_expires_in: Option<i64>, - scope: Option<String>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_scope")] + scope: Vec<String>, refresh_token: Option<String>, user_id: Option<String>, id_token: Option<String>, @@ -32,48 +47,51 @@ struct PhantomAccessToken { additional_fields: HashMap<String, Value>, } -/// OAuth 2.0 Access Token +/// An access token is a security token issued by an authorization server as part of an OAuth 2.0 flow. +/// It contains information about the user and the resource for which the token is intended. +/// The information can be used to access web APIs and other protected resources. +/// Resources validate access tokens to grant access to a client application. +/// For more information, see [Access tokens in the Microsoft Identity Platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// /// Create a new AccessToken. /// # Example /// ``` -/// # use graph_oauth::oauth::MsalTokenResponse; -/// let token_response = MsalTokenResponse::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); +/// # use graph_extensions::token::MsalToken; +/// let token_response = MsalToken::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); /// ``` -/// The [MsalTokenResponse::jwt] method attempts to parse the access token as a JWT. +/// The [MsalToken::jwt] method attempts to parse the access token as a JWT. /// Tokens returned for personal microsoft accounts that use legacy MSA /// are encrypted and cannot be parsed. This bearer token may still be /// valid but the jwt() method will return None. /// For more info see: /// [Microsoft identity platform access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) -/// -/// * Access Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.4 -/// * Refresh Tokens: https://datatracker.ietf.org/doc/html/rfc6749#section-1.5 -/// -/// # Example -/// ``` -/// # use graph_oauth::oauth::MsalTokenResponse; -/// # let mut token = MsalTokenResponse::new("Bearer", 3600, "Read Read.Write", "ASODFIUJ34KJ;LADSK"); -/// -/// // Duration left until expired. -/// println!("{:#?}", token.elapsed()); /// ``` #[derive(Clone, Eq, PartialEq, Serialize)] -pub struct MsalTokenResponse { +pub struct MsalToken { pub access_token: String, pub token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] pub expires_in: i64, /// Legacy version of expires_in pub ext_expires_in: Option<i64>, - pub scope: Option<String>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_scope")] + pub scope: Vec<String>, + + /// Because access tokens are valid for only a short period of time, + /// authorization servers sometimes issue a refresh token at the same + /// time the access token is issued. The client application can then + /// exchange this refresh token for a new access token when needed. + /// For more information, see + /// [Refresh tokens in the Microsoft identity platform.](https://learn.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens) pub refresh_token: Option<String>, pub user_id: Option<String>, pub id_token: Option<String>, pub state: Option<String>, pub correlation_id: Option<String>, pub client_info: Option<String>, - pub timestamp: Option<DateTime<Utc>>, + pub timestamp: Option<time::OffsetDateTime>, + pub expires_on: Option<time::OffsetDateTime>, /// Any extra returned fields for AccessToken. #[serde(flatten)] pub additional_fields: HashMap<String, Value>, @@ -81,18 +99,21 @@ pub struct MsalTokenResponse { log_pii: bool, } -impl MsalTokenResponse { - pub fn new( +impl MsalToken { + pub fn new<T: ToString, I: IntoIterator<Item = T>>( token_type: &str, expires_in: i64, - scope: &str, access_token: &str, - ) -> MsalTokenResponse { - MsalTokenResponse { + scope: I, + ) -> MsalToken { + let mut timestamp = time::OffsetDateTime::now_utc(); + let expires_on = timestamp.add(time::Duration::seconds(expires_in)); + + MsalToken { token_type: token_type.into(), - ext_expires_in: Some(expires_in), + ext_expires_in: None, expires_in, - scope: Some(scope.into()), + scope: scope.into_iter().map(|s| s.to_string()).collect(), access_token: access_token.into(), refresh_token: None, user_id: None, @@ -100,7 +121,8 @@ impl MsalTokenResponse { state: None, correlation_id: None, client_info: None, - timestamp: Some(Utc::now() + Duration::seconds(expires_in)), + timestamp: Some(timestamp), + expires_on: Some(expires_on), additional_fields: Default::default(), log_pii: false, } @@ -110,12 +132,12 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_token_type("Bearer"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_token_type("Bearer"); /// ``` - pub fn set_token_type(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn with_token_type(&mut self, s: &str) -> &mut Self { self.token_type = s.into(); self } @@ -124,14 +146,16 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_expires_in(3600); + /// let mut access_token = MsalToken::default(); + /// access_token.with_expires_in(3600); /// ``` - pub fn set_expires_in(&mut self, expires_in: i64) -> &mut MsalTokenResponse { + pub fn with_expires_in(&mut self, expires_in: i64) -> &mut Self { self.expires_in = expires_in; - self.timestamp = Some(Utc::now() + Duration::seconds(expires_in)); + let timestamp = time::OffsetDateTime::now_utc(); + self.expires_on = Some(timestamp.add(time::Duration::seconds(self.expires_in.clone()))); + self.timestamp = Some(timestamp); self } @@ -139,13 +163,13 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_scope("Read Read.Write"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_scope(vec!["User.Read"]); /// ``` - pub fn set_scope(&mut self, s: &str) -> &mut MsalTokenResponse { - self.scope = Some(s.to_string()); + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } @@ -153,12 +177,12 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_bearer_token("ASODFIUJ34KJ;LADSK"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_access_token("ASODFIUJ34KJ;LADSK"); /// ``` - pub fn set_bearer_token(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn with_access_token(&mut self, s: &str) -> &mut Self { self.access_token = s.into(); self } @@ -167,12 +191,12 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_refresh_token("#ASOD323U5342"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_refresh_token("#ASOD323U5342"); /// ``` - pub fn set_refresh_token(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn with_refresh_token(&mut self, s: &str) -> &mut Self { self.refresh_token = Some(s.to_string()); self } @@ -181,12 +205,12 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_user_id("user_id"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_user_id("user_id"); /// ``` - pub fn set_user_id(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn with_user_id(&mut self, s: &str) -> &mut Self { self.user_id = Some(s.to_string()); self } @@ -195,12 +219,12 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{MsalTokenResponse, IdToken}; + /// # use graph_extensions::token::{MsalToken, IdToken}; /// - /// let mut access_token = MsalTokenResponse::default(); + /// let mut access_token = MsalToken::default(); /// access_token.set_id_token("id_token"); /// ``` - pub fn set_id_token(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn set_id_token(&mut self, s: &str) -> &mut Self { self.id_token = Some(s.to_string()); self } @@ -209,13 +233,13 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{MsalTokenResponse, IdToken}; + /// # use graph_extensions::token::{MsalToken, IdToken}; /// - /// let mut access_token = MsalTokenResponse::default(); + /// let mut access_token = MsalToken::default(); /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); /// ``` pub fn with_id_token(&mut self, id_token: IdToken) { - self.id_token = Some(id_token.get_id_token()); + self.id_token = Some(id_token.id_token); } pub fn parse_id_token(&mut self) -> Option<Result<IdToken, serde::de::value::Error>> { @@ -226,20 +250,20 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; - /// # use graph_oauth::oauth::IdToken; + /// # use graph_extensions::token::MsalToken; + /// # use graph_extensions::token::IdToken; /// - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_state("state"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_state("state"); /// ``` - pub fn set_state(&mut self, s: &str) -> &mut MsalTokenResponse { + pub fn with_state(&mut self, s: &str) -> &mut Self { self.state = Some(s.to_string()); self } /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled - /// passing [MsalTokenResponse] to logging or print functions will log both the bearer + /// passing [MsalToken] to logging or print functions will log both the bearer /// access token value, the refresh token value if any, and the id token value. /// By default these do not get logged. pub fn enable_pii_logging(&mut self, log_pii: bool) { @@ -260,42 +284,46 @@ impl MsalTokenResponse { /// from when the token was first retrieved. /// /// This will reset the the timestamp from Utc Now + expires_in. This means - /// that if calling [MsalTokenResponse::gen_timestamp] will only be reliable if done + /// that if calling [MsalToken::gen_timestamp] will only be reliable if done /// when the access token is first retrieved. /// /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); + /// let mut access_token = MsalToken::default(); /// access_token.expires_in = 86999; /// access_token.gen_timestamp(); /// println!("{:#?}", access_token.timestamp); /// // The timestamp is in UTC. /// ``` pub fn gen_timestamp(&mut self) { - self.timestamp = Some(Utc::now() + Duration::seconds(self.expires_in)); + let mut timestamp = time::OffsetDateTime::now_utc(); + let expires_on = timestamp.add(time::Duration::seconds(self.expires_in.clone())); + self.timestamp = Some(timestamp); + self.expires_on = Some(expires_on); } /// Check whether the access token is expired. Uses the expires_in /// field to check time elapsed since token was first deserialized. - /// This is done using a Utc timestamp set when the [MsalTokenResponse] is + /// This is done using a Utc timestamp set when the [MsalToken] is /// deserialized from the api response /// /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); + /// let mut access_token = MsalToken::default(); /// println!("{:#?}", access_token.is_expired()); /// ``` pub fn is_expired(&self) -> bool { - if let Some(human_time) = self.elapsed() { - return human_time.le(&HumanTime::from(Duration::seconds(0))); + if let Some(expires_on) = self.expires_on.as_ref() { + expires_on.lt(&OffsetDateTime::now_utc()) + } else { + false } - true } /// Get the time left in seconds until the access token expires. @@ -305,31 +333,23 @@ impl MsalTokenResponse { /// /// # Example /// ``` - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_extensions::token::MsalToken; /// - /// let mut access_token = MsalTokenResponse::default(); + /// let mut access_token = MsalToken::default(); /// println!("{:#?}", access_token.elapsed()); /// ``` - pub fn elapsed(&self) -> Option<HumanTime> { - if let Some(timestamp) = self.timestamp { - let ht = HumanTime::from(timestamp); - return Some(ht); - } - None - } - - pub fn jwt(&mut self) -> Option<JsonWebToken> { - JwtParser::parse(self.access_token.as_str()).ok() + pub fn elapsed(&self) -> Option<time::Duration> { + Some(self.expires_on? - self.timestamp?) } } -impl Default for MsalTokenResponse { +impl Default for MsalToken { fn default() -> Self { - MsalTokenResponse { + MsalToken { token_type: String::new(), expires_in: 0, - ext_expires_in: Some(0), - scope: None, + ext_expires_in: None, + scope: vec![], access_token: String::new(), refresh_token: None, user_id: None, @@ -337,14 +357,18 @@ impl Default for MsalTokenResponse { state: None, correlation_id: None, client_info: None, - timestamp: Some(Utc::now() + Duration::seconds(0)), + timestamp: Some(time::OffsetDateTime::now_utc()), + expires_on: Some( + time::OffsetDateTime::from_unix_timestamp(0) + .unwrap_or(time::OffsetDateTime::UNIX_EPOCH), + ), additional_fields: Default::default(), log_pii: false, } } } -impl TryFrom<&str> for MsalTokenResponse { +impl TryFrom<&str> for MsalToken { type Error = GraphFailure; fn try_from(value: &str) -> Result<Self, Self::Error> { @@ -352,35 +376,35 @@ impl TryFrom<&str> for MsalTokenResponse { } } -impl TryFrom<reqwest::blocking::RequestBuilder> for MsalTokenResponse { +impl TryFrom<reqwest::blocking::RequestBuilder> for MsalToken { type Error = GraphFailure; fn try_from(value: reqwest::blocking::RequestBuilder) -> Result<Self, Self::Error> { let response = value.send()?; - MsalTokenResponse::try_from(response) + MsalToken::try_from(response) } } -impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for MsalTokenResponse { +impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for MsalToken { type Error = GraphFailure; fn try_from( value: Result<reqwest::blocking::Response, reqwest::Error>, ) -> Result<Self, Self::Error> { let response = value?; - MsalTokenResponse::try_from(response) + MsalToken::try_from(response) } } -impl TryFrom<reqwest::blocking::Response> for MsalTokenResponse { +impl TryFrom<reqwest::blocking::Response> for MsalToken { type Error = GraphFailure; fn try_from(value: reqwest::blocking::Response) -> Result<Self, Self::Error> { - Ok(value.json::<MsalTokenResponse>()?) + Ok(value.json::<MsalToken>()?) } } -impl fmt::Debug for MsalTokenResponse { +impl fmt::Debug for MsalToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.log_pii { f.debug_struct("MsalAccessToken") @@ -393,6 +417,12 @@ impl fmt::Debug for MsalTokenResponse { .field("id_token", &self.id_token) .field("state", &self.state) .field("timestamp", &self.timestamp) + .field("expires_on", &self.expires_on) + .field( + "expires_result", + &time::OffsetDateTime::now_utc() + .checked_add(time::Duration::seconds(self.expires_in.clone())), + ) .field("additional_fields", &self.additional_fields) .finish() } else { @@ -415,25 +445,35 @@ impl fmt::Debug for MsalTokenResponse { ) .field("state", &self.state) .field("timestamp", &self.timestamp) + .field("expires_on", &self.expires_on) + .field( + "expires_result", + &time::OffsetDateTime::now_utc() + .checked_add(time::Duration::seconds(self.expires_in.clone())), + ) .field("additional_fields", &self.additional_fields) .finish() } } } -impl AsRef<str> for MsalTokenResponse { +impl AsRef<str> for MsalToken { fn as_ref(&self) -> &str { self.access_token.as_str() } } -impl<'de> Deserialize<'de> for MsalTokenResponse { +impl<'de> Deserialize<'de> for MsalToken { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { - let phantom_access_token: PhantomAccessToken = Deserialize::deserialize(deserializer)?; - Ok(MsalTokenResponse { + let phantom_access_token: PhantomMsalToken = Deserialize::deserialize(deserializer)?; + + let mut timestamp = time::OffsetDateTime::now_utc(); + let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); + + Ok(MsalToken { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, expires_in: phantom_access_token.expires_in, @@ -445,9 +485,58 @@ impl<'de> Deserialize<'de> for MsalTokenResponse { state: phantom_access_token.state, correlation_id: phantom_access_token.correlation_id, client_info: phantom_access_token.client_info, - timestamp: Some(Utc::now() + Duration::seconds(phantom_access_token.expires_in)), + timestamp: Some(timestamp), + expires_on: Some(expires_on), additional_fields: phantom_access_token.additional_fields, log_pii: false, }) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn is_expired_test() { + let mut access_token = MsalToken::default(); + access_token.with_expires_in(1); + std::thread::sleep(std::time::Duration::from_secs(3)); + assert!(access_token.is_expired()); + + let mut access_token = MsalToken::default(); + access_token.with_expires_in(10); + std::thread::sleep(std::time::Duration::from_secs(4)); + assert!(!access_token.is_expired()); + } + + pub const ACCESS_TOKEN_INT: &str = r#"{ + "access_token": "fasdfasdfasfdasdfasfsdf", + "token_type": "Bearer", + "expires_in": 65874, + "scope": null, + "refresh_token": null, + "user_id": "santa@north.pole.com", + "id_token": "789aasdf-asdf", + "state": null, + "timestamp": "2020-10-27T16:31:38.788098400Z" + }"#; + + pub const ACCESS_TOKEN_STRING: &str = r#"{ + "access_token": "fasdfasdfasfdasdfasfsdf", + "token_type": "Bearer", + "expires_in": "65874", + "scope": null, + "refresh_token": null, + "user_id": "helpers@north.pole.com", + "id_token": "789aasdf-asdf", + "state": null, + "timestamp": "2020-10-27T16:31:38.788098400Z" + }"#; + + #[test] + pub fn test_deserialize() { + let _token: MsalToken = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); + let _token: MsalToken = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); + } +} diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-extensions/src/web/interactive_authenticator.rs similarity index 79% rename from graph-oauth/src/web/interactive_authenticator.rs rename to graph-extensions/src/web/interactive_authenticator.rs index 88f76c47..bca1e680 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-extensions/src/web/interactive_authenticator.rs @@ -1,10 +1,9 @@ -use crate::identity::AuthorizationUrl; -use crate::web::InteractiveWebViewOptions; +use crate::web::WebViewOptions; -pub trait InteractiveAuthenticator: AuthorizationUrl { +pub trait InteractiveAuthenticator { fn interactive_authentication( &self, - interactive_web_view_options: Option<InteractiveWebViewOptions>, + interactive_web_view_options: Option<WebViewOptions>, ) -> anyhow::Result<Option<String>>; } diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-extensions/src/web/interactive_web_view.rs similarity index 98% rename from graph-oauth/src/web/interactive_web_view.rs rename to graph-extensions/src/web/interactive_web_view.rs index 217d52ba..7102c07a 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-extensions/src/web/interactive_web_view.rs @@ -2,7 +2,7 @@ use anyhow::Context; use std::time::Duration; use url::Url; -use crate::web::InteractiveWebViewOptions; +use crate::web::WebViewOptions; use wry::application::platform::windows::EventLoopExtWindows; use wry::{ application::{ @@ -63,7 +63,7 @@ impl InteractiveWebView { pub fn interactive_authentication( uri: Url, redirect_uri: Url, - options: InteractiveWebViewOptions, + options: WebViewOptions, sender: std::sync::mpsc::Sender<String>, ) -> anyhow::Result<()> { let event_loop: EventLoop<UserEvents> = EventLoop::<UserEvents>::new_any_thread(); diff --git a/graph-oauth/src/web/mod.rs b/graph-extensions/src/web/mod.rs similarity index 63% rename from graph-oauth/src/web/mod.rs rename to graph-extensions/src/web/mod.rs index 2fe21acd..904c2db8 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-extensions/src/web/mod.rs @@ -1,7 +1,7 @@ mod interactive_authenticator; mod interactive_web_view; -mod interactive_web_view_options; +mod web_view_options; pub use interactive_authenticator::*; pub use interactive_web_view::*; -pub use interactive_web_view_options::*; +pub use web_view_options::*; diff --git a/graph-oauth/src/web/interactive_web_view_options.rs b/graph-extensions/src/web/web_view_options.rs similarity index 84% rename from graph-oauth/src/web/interactive_web_view_options.rs rename to graph-extensions/src/web/web_view_options.rs index c87c6f70..e7792de1 100644 --- a/graph-oauth/src/web/interactive_web_view_options.rs +++ b/graph-extensions/src/web/web_view_options.rs @@ -1,7 +1,7 @@ use std::time::Duration; #[derive(Clone)] -pub struct InteractiveWebViewOptions { +pub struct WebViewOptions { pub panic_on_invalid_uri_navigation_attempt: bool, pub theme: Option<wry::application::window::Theme>, /// Provide a list of ports to use for interactive authentication. @@ -11,9 +11,9 @@ pub struct InteractiveWebViewOptions { pub timeout: Duration, } -impl Default for InteractiveWebViewOptions { +impl Default for WebViewOptions { fn default() -> Self { - InteractiveWebViewOptions { + WebViewOptions { panic_on_invalid_uri_navigation_attempt: true, theme: None, ports: vec![], diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 111952e6..1e57f21c 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -26,6 +26,7 @@ url = { version = "2", features = ["serde"] } graph-error = { path = "../graph-error" } graph-core = { path = "../graph-core" } graph-extensions = { path = "../graph-extensions" } +graph-oauth = { path = "../graph-oauth", version = "1.0.2", default-features=false } [features] default = ["native-tls"] @@ -34,3 +35,4 @@ rustls-tls = ["reqwest/rustls-tls"] brotli = ["reqwest/brotli"] deflate = ["reqwest/deflate"] trust-dns = ["reqwest/trust-dns"] +openssl = ["graph-oauth/openssl"] diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 893014ba..cc509b9a 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,4 +1,13 @@ use crate::blocking::BlockingClient; +use async_trait::async_trait; +use graph_error::AuthExecutionResult; +use graph_extensions::cache::{ + InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, +}; +use graph_extensions::token::{ClientApplication, ClientApplicationType}; +use graph_oauth::oauth::{ + ConfidentialClientApplication, PublicClientApplication, UnInitializedCredentialExecutor, +}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -12,9 +21,57 @@ fn user_agent_header_from_env() -> Option<HeaderValue> { HeaderValue::from_str(header).ok() } +#[derive(Clone)] +pub struct BearerToken(pub String); + +impl TokenStore for BearerToken { + fn token_store_provider(&self) -> TokenStoreProvider { + TokenStoreProvider::InMemory + } + + fn is_stored_token_initialized(&self, id: &str) -> bool { + true + } + + fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { + None + } + + fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { + None + } + + fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { + Some(&self.0) + } + + fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { + None + } +} + +#[async_trait] +impl ClientApplication for BearerToken { + fn client_application_type(&self) -> ClientApplicationType { + ClientApplicationType::PublicClientApplication + } + + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + Ok(self.0.clone()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + Ok(self.0.clone()) + } + + fn get_stored_application_token(&mut self) -> Option<&StoredToken> { + None + } +} + #[derive(Clone)] struct ClientConfiguration { - access_token: Option<String>, + client_application: Option<Box<dyn ClientApplication>>, headers: HeaderMap, referer: bool, timeout: Option<Duration>, @@ -34,7 +91,7 @@ impl ClientConfiguration { } ClientConfiguration { - access_token: None, + client_application: None, headers, referer: true, timeout: None, @@ -74,7 +131,25 @@ impl GraphClientConfiguration { } pub fn access_token<AT: ToString>(mut self, access_token: AT) -> GraphClientConfiguration { - self.config.access_token = Some(access_token.to_string()); + self.config.client_application = Some(Box::new(BearerToken(access_token.to_string()))); + self + } + + pub fn client_application<CA: ClientApplication + 'static>(mut self, client_app: CA) -> Self { + self.config.client_application = Some(Box::new(client_app)); + self + } + + pub fn confidential_client_application( + mut self, + confidential_client: ConfidentialClientApplication, + ) -> Self { + self.config.client_application = Some(Box::new(confidential_client)); + self + } + + pub fn public_client_application(mut self, public_client: PublicClientApplication) -> Self { + self.config.client_application = Some(Box::new(public_client)); self } @@ -157,11 +232,20 @@ impl GraphClientConfiguration { builder = builder.connect_timeout(connect_timeout); } - Client { - access_token: self.config.access_token.unwrap_or_default(), - inner: builder.build().unwrap(), - headers, - builder: config, + if let Some(client_application) = self.config.client_application { + Client { + client_application, + inner: builder.build().unwrap(), + headers, + builder: config, + } + } else { + Client { + client_application: Box::new(BearerToken(Default::default())), + inner: builder.build().unwrap(), + headers, + builder: config, + } } } @@ -184,7 +268,7 @@ impl GraphClientConfiguration { } BlockingClient { - access_token: self.config.access_token.unwrap_or_default(), + access_token: Default::default(), inner: builder.build().unwrap(), headers, } @@ -199,16 +283,22 @@ impl Default for GraphClientConfiguration { #[derive(Clone)] pub struct Client { - pub(crate) access_token: String, + pub(crate) client_application: Box<dyn ClientApplication>, pub(crate) inner: reqwest::Client, pub(crate) headers: HeaderMap, pub(crate) builder: GraphClientConfiguration, } impl Client { - pub fn new<AT: ToString>(access_token: AT) -> Client { + pub fn new<CA: ClientApplication + 'static>(client_app: CA) -> Self { + GraphClientConfiguration::new() + .client_application(client_app) + .build() + } + + pub fn from_access_token<T: AsRef<str>>(access_token: T) -> Self { GraphClientConfiguration::new() - .access_token(access_token) + .access_token(access_token.as_ref()) .build() } @@ -227,6 +317,14 @@ impl Client { pub fn headers(&self) -> &HeaderMap { &self.headers } + + pub fn get_token(&mut self) -> Option<String> { + self.client_application.get_token_silent().ok() + } + + pub fn get_token2(&mut self) -> Option<&StoredToken> { + self.client_application.get_stored_application_token() + } } impl Default for Client { @@ -245,9 +343,16 @@ impl Debug for Client { } } +impl From<ConfidentialClientApplication> for Client { + fn from(value: ConfidentialClientApplication) -> Self { + todo!() + } +} + #[cfg(test)] mod test { use super::*; + use graph_oauth::oauth::ConfidentialClientApplication; #[test] fn compile_time_user_agent_header() { @@ -269,4 +374,18 @@ mod test { let user_agent_header = client.builder.config.headers.get(USER_AGENT).unwrap(); assert_eq!("user_agent", user_agent_header.to_str().unwrap()); } + + /* + #[test] + fn initialize_confidential_client() { + let client = GraphClientConfiguration::new() + .access_token("access_token") + .user_agent(HeaderValue::from_static("user_agent")) + .build_with_client_application(ConfidentialClientApplication::builder("client-id") + .with_client_secret("secret") + .build()); + + assert!(client.client_application.get_stored_token()); + } + */ } diff --git a/graph-http/src/request_handler.rs b/graph-http/src/request_handler.rs index f968ea79..f8b4b6ac 100644 --- a/graph-http/src/request_handler.rs +++ b/graph-http/src/request_handler.rs @@ -30,6 +30,9 @@ impl RequestHandler { err: Option<GraphFailure>, body: Option<BodyRead>, ) -> RequestHandler { + let mut token = inner.clone(); + let access_token = token.get_token().unwrap(); + let mut original_headers = inner.headers; original_headers.extend(request_components.headers.clone()); request_components.headers = original_headers; @@ -45,7 +48,7 @@ impl RequestHandler { RequestHandler { inner: inner.inner.clone(), - access_token: inner.access_token, + access_token, request_components, error, body, diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 47785cea..dc912aa9 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -21,6 +21,7 @@ async-trait = "0.1.35" base64 = "0.21.0" chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" +dyn-clone = "1.0.14" hex = "0.4.3" http = "0.2.9" openssl = { version = "0.10", optional=true } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index bbdb5a92..5c61ea3a 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,12 +1,11 @@ -use crate::access_token::MsalTokenResponse; use crate::grants::{GrantRequest, GrantType}; -use crate::id_token::IdToken; use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult, AF}; +use graph_extensions::token::{IdToken, MsalToken}; use ring::rand::SecureRandom; use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::{BTreeSet, HashMap}; @@ -133,7 +132,7 @@ impl AsRef<str> for OAuthParameter { /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct OAuthSerializer { - access_token: Option<MsalTokenResponse>, + access_token: Option<MsalToken>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, } @@ -901,12 +900,12 @@ impl OAuthSerializer { /// # Example /// ``` /// use graph_oauth::oauth::OAuthSerializer; - /// use graph_oauth::oauth::MsalTokenResponse; + /// use graph_oauth::oauth::MsalToken; /// let mut oauth = OAuthSerializer::new(); - /// let access_token = MsalTokenResponse::default(); + /// let access_token = MsalToken::default(); /// oauth.access_token(access_token); /// ``` - pub fn access_token(&mut self, ac: MsalTokenResponse) { + pub fn access_token(&mut self, ac: MsalToken) { if let Some(refresh_token) = ac.refresh_token.as_ref() { self.refresh_token(refresh_token.as_str()); } @@ -918,14 +917,14 @@ impl OAuthSerializer { /// # Example /// ``` /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::MsalTokenResponse; - /// # let access_token = MsalTokenResponse::default(); + /// # use graph_oauth::oauth::MsalToken; + /// # let access_token = MsalToken::default(); /// # let mut oauth = OAuthSerializer::new(); /// # oauth.access_token(access_token); /// let access_token = oauth.get_access_token().unwrap(); /// println!("{:#?}", access_token); /// ``` - pub fn get_access_token(&self) -> Option<MsalTokenResponse> { + pub fn get_access_token(&self) -> Option<MsalToken> { self.access_token.clone() } @@ -936,10 +935,10 @@ impl OAuthSerializer { /// # Example /// ``` /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::MsalTokenResponse; + /// # use graph_oauth::oauth::MsalToken; /// # let mut oauth = OAuthSerializer::new(); - /// let mut access_token = MsalTokenResponse::default(); - /// access_token.set_refresh_token("refresh_token"); + /// let mut access_token = MsalToken::default(); + /// access_token.with_refresh_token("refresh_token"); /// oauth.access_token(access_token); /// /// let refresh_token = oauth.get_refresh_token().unwrap(); @@ -959,6 +958,7 @@ impl OAuthSerializer { } } + #[deprecated] pub fn build(&mut self) -> GrantSelector<AccessTokenGrant> { GrantSelector { oauth: self.clone(), @@ -966,6 +966,7 @@ impl OAuthSerializer { } } + #[deprecated] pub fn build_async(&mut self) -> GrantSelector<AsyncAccessTokenGrant> { GrantSelector { oauth: self.clone(), @@ -1349,6 +1350,7 @@ impl<V: ToString> Extend<(OAuthParameter, V)> for OAuthSerializer { } } +#[deprecated] pub struct GrantSelector<T> { oauth: OAuthSerializer, t: PhantomData<T>, @@ -1646,6 +1648,7 @@ impl GrantSelector<AsyncAccessTokenGrant> { } } +#[deprecated] #[derive(Debug)] pub struct AuthorizationRequest { uri: String, @@ -1662,6 +1665,7 @@ impl AuthorizationRequest { } } +#[deprecated] #[derive(Debug)] pub struct AccessTokenRequest { uri: String, @@ -1671,7 +1675,7 @@ pub struct AccessTokenRequest { impl AccessTokenRequest { /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [MsalTokenResponse] + /// should be an access token which you can convert to [MsalToken] /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example @@ -1741,6 +1745,7 @@ impl AccessTokenRequest { } } +#[deprecated] #[derive(Debug)] pub struct AsyncAccessTokenRequest { uri: String, @@ -1750,7 +1755,7 @@ pub struct AsyncAccessTokenRequest { impl AsyncAccessTokenRequest { /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [MsalTokenResponse] + /// should be an access token which you can convert to [MsalToken] /// and pass back to [OAuthSerializer] to use to get refresh tokens. /// /// # Example @@ -1821,6 +1826,7 @@ impl AsyncAccessTokenRequest { } } +#[deprecated] #[derive(Debug)] pub struct ImplicitGrant { oauth: OAuthSerializer, @@ -1881,6 +1887,7 @@ impl AsRef<OAuthSerializer> for ImplicitGrant { } } +#[deprecated] pub struct DeviceCodeGrant { oauth: OAuthSerializer, grant: GrantType, @@ -1998,6 +2005,7 @@ impl DeviceCodeGrant { } } +#[deprecated] pub struct AsyncDeviceCodeGrant { oauth: OAuthSerializer, grant: GrantType, @@ -2115,6 +2123,7 @@ impl AsyncDeviceCodeGrant { } } +#[deprecated] #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AccessTokenGrant { oauth: OAuthSerializer, @@ -2278,6 +2287,7 @@ impl AccessTokenGrant { } } +#[deprecated] #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AsyncAccessTokenGrant { oauth: OAuthSerializer, diff --git a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs b/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs deleted file mode 100644 index 2812d04c..00000000 --- a/graph-oauth/src/identity/credential_store/in_memory_credential_store.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::access_token::MsalTokenResponse; -use crate::identity::{CredentialStore, CredentialStoreType, TokenCacheProviderType}; -use std::collections::BTreeMap; - -#[derive(Clone, Default)] -pub struct InMemoryCredentialStore { - credentials: BTreeMap<String, CredentialStoreType>, -} - -impl InMemoryCredentialStore { - pub fn new() -> InMemoryCredentialStore { - InMemoryCredentialStore { - credentials: BTreeMap::new(), - } - } - - pub fn from_bearer_token<T: AsRef<str>>( - client_id: T, - bearer_token: T, - ) -> InMemoryCredentialStore { - let mut credentials = BTreeMap::new(); - credentials.insert( - client_id.as_ref().to_owned(), - CredentialStoreType::Bearer(bearer_token.as_ref().to_owned()), - ); - InMemoryCredentialStore { credentials } - } - - pub fn from_access_token<T: AsRef<str>>( - client_id: T, - access_token: MsalTokenResponse, - ) -> InMemoryCredentialStore { - let mut credentials = BTreeMap::new(); - credentials.insert( - client_id.as_ref().to_owned(), - CredentialStoreType::AccessToken(access_token), - ); - InMemoryCredentialStore { credentials } - } -} - -impl CredentialStore for InMemoryCredentialStore { - fn token_cache_provider(&self) -> TokenCacheProviderType { - TokenCacheProviderType::InMemory - } - - fn get_token_by_client_id(&self, client_id: &str) -> &CredentialStoreType { - info!("InMemoryCredentialStore"); - self.credentials - .get(client_id) - .unwrap_or(&CredentialStoreType::UnInitialized) - } - - fn update_by_client_id(&mut self, client_id: &str, credential_store_type: CredentialStoreType) { - info!("InMemoryCredentialStore"); - self.credentials - .insert(client_id.to_owned(), credential_store_type); - } -} diff --git a/graph-oauth/src/identity/credential_store/mod.rs b/graph-oauth/src/identity/credential_store/mod.rs deleted file mode 100644 index f169648b..00000000 --- a/graph-oauth/src/identity/credential_store/mod.rs +++ /dev/null @@ -1,45 +0,0 @@ -mod in_memory_credential_store; -mod token_cache_providers; - -pub use in_memory_credential_store::*; -pub use token_cache_providers::*; - -use crate::oauth::MsalTokenResponse; - -#[derive(Debug, Clone, Eq, PartialEq)] -#[allow(clippy::large_enum_variant)] -pub enum CredentialStoreType { - Bearer(String), - AccessToken(MsalTokenResponse), - UnInitialized, -} - -pub trait CredentialStore { - fn token_cache_provider(&self) -> TokenCacheProviderType { - TokenCacheProviderType::UnInitialized - } - - fn get_token(&self) -> &CredentialStoreType { - &CredentialStoreType::UnInitialized - } - - fn update(&mut self, _credential_store_type: CredentialStoreType) {} - - fn get_token_by_client_id(&self, client_id: &str) -> &CredentialStoreType; - - fn update_by_client_id( - &mut self, - _client_id: &str, - _credential_store_type: CredentialStoreType, - ) { - } -} - -pub(crate) struct UnInitializedCredentialStore; - -impl CredentialStore for UnInitializedCredentialStore { - fn get_token_by_client_id(&self, _client_id: &str) -> &CredentialStoreType { - info!("UnInitializedCredentialStore"); - &CredentialStoreType::UnInitialized - } -} diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index c37c5395..5517aacc 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,4 +1,5 @@ use crate::identity::{Authority, AzureCloudInstance}; +use graph_extensions::cache::{TokenStore, UnInitializedTokenStore}; use reqwest::header::HeaderMap; use std::collections::HashMap; use url::Url; @@ -24,7 +25,6 @@ pub struct AppConfig { /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. pub(crate) redirect_uri: Option<Url>, - pub(crate) token_store: HashMap<String, String>, } impl AppConfig { @@ -37,7 +37,6 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, - token_store: Default::default(), } } @@ -50,7 +49,6 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, - token_store: Default::default(), } } @@ -66,7 +64,14 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, - token_store: Default::default(), + } + } + + pub(crate) fn cache_id(&self) -> String { + if let Some(tenant_id) = self.tenant_id.as_ref() { + format!("{},{}", tenant_id, self.client_id.to_string()) + } else { + self.client_id.to_string() } } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 5172ab07..fe4f6846 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -5,7 +5,8 @@ use crate::identity::{ AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, - OpenIdCredentialBuilder, PublicClientApplication, + OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredential, + ResourceOwnerPasswordCredentialBuilder, }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; @@ -59,7 +60,7 @@ impl ConfidentialClientApplicationBuilder { ConfidentialClientApplicationBuilder::try_from(application_options) } - pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { let tenant = tenant_id.as_ref().to_string(); self.app_config.tenant_id = Some(tenant.clone()); self.app_config.authority = Authority::TenantId(tenant); @@ -231,7 +232,6 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, - token_store: Default::default(), }, }) } @@ -257,7 +257,7 @@ impl PublicClientApplicationBuilder { PublicClientApplicationBuilder::try_from(application_options) } - pub fn with_tenant_id(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { let tenant = tenant_id.as_ref().to_string(); self.app_config.tenant_id = Some(tenant.clone()); self.app_config.authority = Authority::TenantId(tenant); @@ -321,8 +321,19 @@ impl PublicClientApplicationBuilder { } */ - pub fn with_resource_owner_password_from_environment( - ) -> Result<PublicClientApplication, VarError> { + pub fn with_username_password( + self, + username: impl AsRef<str>, + password: impl AsRef<str>, + ) -> ResourceOwnerPasswordCredentialBuilder { + ResourceOwnerPasswordCredentialBuilder::new_with_username_password( + username.as_ref(), + password.as_ref(), + self.app_config, + ) + } + + pub fn with_username_password_from_environment() -> Result<PublicClientApplication, VarError> { EnvironmentCredential::resource_owner_password_credential() } } @@ -359,7 +370,6 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, - token_store: Default::default(), }, }) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs index e5bcc5e5..f3159596 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs @@ -4,7 +4,7 @@ use crate::identity::{ ResponseMode, }; use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; -use crate::web::{InteractiveAuthenticator, InteractiveWebViewOptions}; +use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; use graph_error::{AuthorizationResult, AF}; @@ -73,7 +73,6 @@ impl AuthCodeAuthorizationUrlParameters { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - token_store: Default::default(), }, response_type, response_mode: None, @@ -115,7 +114,7 @@ impl AuthCodeAuthorizationUrlParameters { pub fn interactive_webview_authentication( &self, - interactive_web_view_options: Option<InteractiveWebViewOptions>, + interactive_web_view_options: Option<WebViewOptions>, ) -> anyhow::Result<AuthorizationQueryResponse> { let url_string = self .interactive_authentication(interactive_web_view_options)? @@ -159,12 +158,12 @@ impl AuthCodeAuthorizationUrlParameters { mod web_view_authenticator { use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; - use crate::web::{InteractiveAuthenticator, InteractiveWebView, InteractiveWebViewOptions}; + use graph_extensions::web::{InteractiveAuthenticator, InteractiveWebView, WebViewOptions}; impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { fn interactive_authentication( &self, - interactive_web_view_options: Option<InteractiveWebViewOptions>, + interactive_web_view_options: Option<WebViewOptions>, ) -> anyhow::Result<Option<String>> { let uri = self.authorization_url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 017a48d7..abc8334d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -78,7 +78,6 @@ impl AuthorizationCodeCertificateCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri, - token_store: Default::default(), }; Ok(AuthorizationCodeCertificateCredential { @@ -111,9 +110,10 @@ impl AuthorizationCodeCertificateCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.authority()); + .authority(&azure_cloud_instance, &self.authority()); let uri = self .serializer @@ -211,10 +211,6 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { &self.app_config.client_id } - fn app_config(&self) -> &AppConfig { - &self.app_config - } - fn authority(&self) -> Authority { self.app_config.authority.clone() } @@ -222,6 +218,10 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn azure_cloud_instance(&self) -> AzureCloudInstance { self.app_config.azure_cloud_instance } + + fn app_config(&self) -> &AppConfig { + &self.app_config + } } #[derive(Clone)] diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index b6f862ee..86b24d4d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -94,7 +94,6 @@ impl AuthorizationCodeCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri), - token_store: Default::default(), }; Ok(AuthorizationCodeCredential { @@ -212,9 +211,10 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.authority()); + .authority(&azure_cloud_instance, &self.authority()); let uri = self .serializer @@ -328,42 +328,38 @@ mod test { #[test] fn with_tenant_id_common() { - let credential = AuthorizationCodeCredential::builder(Uuid::new_v4().to_string(), "code") - .with_authority(Authority::TenantId("common".into())) - .build(); + let credential = AuthorizationCodeCredential::builder( + Uuid::new_v4().to_string(), + "secret".to_string(), + "code", + ) + .with_authority(Authority::TenantId("common".into())) + .build(); assert_eq!(credential.authority(), Authority::TenantId("common".into())) } #[test] fn with_tenant_id_adfs() { - let credential = AuthorizationCodeCredential::builder(Uuid::new_v4().to_string(), "code") - .with_authority(Authority::AzureDirectoryFederatedServices) - .build(); + let credential = AuthorizationCodeCredential::builder( + Uuid::new_v4().to_string(), + "secret".to_string(), + "code", + ) + .with_authority(Authority::AzureDirectoryFederatedServices) + .build(); assert_eq!(credential.authority().as_ref(), "adfs"); } - #[test] - #[should_panic] - fn authorization_code_missing_required_value() { - let mut credential_builder = - AuthorizationCodeCredentialBuilder::new(Uuid::new_v4().to_string(), "code"); - credential_builder - .with_redirect_uri("https://localhost:8080") - .unwrap() - .with_client_secret("client_secret") - .with_scope(vec!["scope"]) - .with_tenant("tenant_id"); - let mut credential = credential_builder.build(); - let _ = credential.form_urlencode().unwrap(); - } - #[test] #[should_panic] fn required_value_missing_client_id() { - let mut credential_builder = - AuthorizationCodeCredential::builder(Uuid::default().to_string(), "code"); + let mut credential_builder = AuthorizationCodeCredential::builder( + Uuid::default().to_string(), + "secret".to_string(), + "code", + ); credential_builder .with_authorization_code("code") .with_refresh_token("token"); @@ -375,7 +371,7 @@ mod test { fn serialization() { let uuid_value = Uuid::new_v4().to_string(); let mut credential_builder = - AuthorizationCodeCredential::builder(uuid_value.clone(), "code"); + AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); let mut credential = credential_builder .with_redirect_uri("https://localhost") .unwrap() diff --git a/graph-oauth/src/identity/credentials/client_application.rs b/graph-oauth/src/identity/credentials/client_application.rs deleted file mode 100644 index 1bba594f..00000000 --- a/graph-oauth/src/identity/credentials/client_application.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::identity::{CredentialStoreType, TokenCredentialExecutor}; -use crate::oauth::MsalTokenResponse; -use async_trait::async_trait; - -#[async_trait] -pub trait ClientApplication: TokenCredentialExecutor { - fn get_credential_from_store(&self) -> &CredentialStoreType; - - fn update_token_credential_store(&mut self, credential_store_type: CredentialStoreType); - - fn get_token_credential(&mut self) -> anyhow::Result<CredentialStoreType> { - debug!("get_token_credential"); - let credential_from_store = self.get_credential_from_store(); - if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { - Ok(credential_from_store.clone()) - } else { - let response = self.execute()?; - let token_value: serde_json::Value = response.json()?; - let bearer = token_value.to_string(); - let access_token_result: serde_json::Result<MsalTokenResponse> = - serde_json::from_value(token_value); - match access_token_result { - Ok(access_token) => { - let credential_store_type = CredentialStoreType::AccessToken(access_token); - self.update_token_credential_store(credential_store_type.clone()); - Ok(credential_store_type) - } - Err(_) => { - let credential_store_type = CredentialStoreType::Bearer(bearer); - self.update_token_credential_store(credential_store_type.clone()); - Ok(credential_store_type) - } - } - } - } - - async fn get_token_credential_async(&mut self) -> anyhow::Result<CredentialStoreType> { - debug!("get_token_credential"); - let credential_from_store = self.get_credential_from_store(); - if !credential_from_store.eq(&CredentialStoreType::UnInitialized) { - Ok(credential_from_store.clone()) - } else { - let response = self.execute_async().await?; - let token_value: serde_json::Value = response.json().await?; - let bearer = token_value.to_string(); - let access_token_result: serde_json::Result<MsalTokenResponse> = - serde_json::from_value(token_value); - match access_token_result { - Ok(access_token) => { - let credential_store_type = CredentialStoreType::AccessToken(access_token); - self.update_token_credential_store(credential_store_type.clone()); - Ok(credential_store_type) - } - Err(_) => { - let credential_store_type = CredentialStoreType::Bearer(bearer); - self.update_token_credential_store(credential_store_type.clone()); - Ok(credential_store_type) - } - } - } - } -} diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 241d369f..92d74716 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -94,9 +94,10 @@ impl ClientAssertionCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.authority()); + .authority(&azure_cloud_instance, &self.authority()); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 7e3bfdd9..76b688ac 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -76,9 +76,10 @@ impl ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.app_config.authority); + .authority(&azure_cloud_instance, &self.authority()); let uri = self .serializer diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 649be919..68802e5c 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -31,7 +31,6 @@ impl ClientCredentialsAuthorizationUrl { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri), - token_store: Default::default(), }, state: None, }) diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index a6b5a9a0..a3863f7b 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -77,9 +77,10 @@ impl ClientSecretCredential { #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.authority()); + .authority(&azure_cloud_instance, &self.authority()); let uri = self.serializer diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index b69bfc25..8445b294 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -4,12 +4,18 @@ use crate::identity::credentials::client_assertion_credential::ClientAssertionCr use crate::identity::{ Authority, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, - TokenCredentialExecutor, + TokenCredentialExecutor, UnInitializedCredentialExecutor, }; use async_trait::async_trait; -use graph_error::{AuthExecutionResult, AuthorizationResult}; +use graph_error::{AuthExecutionResult, AuthorizationResult, AF}; +use crate::oauth::MsalToken; +use graph_extensions::cache::{ + InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, UnInitializedTokenStore, +}; +use graph_extensions::token::{ClientApplication, ClientApplicationType}; +use http::header::ACCEPT; use reqwest::header::{HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; @@ -21,9 +27,11 @@ use wry::http::HeaderMap; /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. +#[derive(Clone)] pub struct ConfidentialClientApplication { http_client: reqwest::Client, credential: Box<dyn TokenCredentialExecutor + Send>, + token_store: Box<dyn TokenStore + Send>, } impl ConfidentialClientApplication { @@ -45,18 +53,149 @@ impl ConfidentialClientApplication { .build() .unwrap(), credential: Box::new(credential), + token_store: Box::new(UnInitializedTokenStore), } } pub fn builder(client_id: &str) -> ConfidentialClientApplicationBuilder { ConfidentialClientApplicationBuilder::new(client_id) } + + pub fn with_in_memory_token_store(&mut self) { + self.token_store = Box::new(InMemoryCredentialStore::new( + self.app_config().cache_id(), + StoredToken::UnInitialized, + )); + } + + fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { + let response = self.get_openid_config()?; + let config: serde_json::Value = response.json()?; + let user_info_endpoint = Url::parse(config["userinfo_endpoint"].as_str().unwrap()).unwrap(); + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT, HeaderValue::from_static("application/json")); + + let cache_id = self.app_config().cache_id(); + let bearer = self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::msg_err( + "TokenStore", + "User Info endpoint requires bearer token - no bearer token found in token cache store", + ))?; + + let response = http_client + .get(user_info_endpoint) + .headers(headers) + .bearer_auth(bearer) + .send() + .expect("Error on header"); + + Ok(response) + } +} + +#[async_trait] +impl ClientApplication for ConfidentialClientApplication { + fn client_application_type(&self) -> ClientApplicationType { + ClientApplicationType::ConfidentialClientApplication + } + + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + let cache_id = self.app_config().cache_id(); + if self.is_store_and_token_initialized(cache_id.as_str()) { + return Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error getting token from store - please report issue", + ))? + .clone()); + } + + if !self.is_token_store_initialized() { + self.with_in_memory_token_store(); + } + + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); + Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error initializing token store - please report issue", + ))? + .clone()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + let cache_id = self.app_config().cache_id(); + if self.is_store_and_token_initialized(cache_id.as_str()) { + return Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error getting token from store - please report issue", + ))? + .clone()); + } + + if !self.is_token_store_initialized() { + self.with_in_memory_token_store(); + } + + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); + Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error initializing token store - please report issue", + ))? + .clone()) + } + + fn get_stored_application_token(&mut self) -> Option<&StoredToken> { + let cache_id = self.app_config().cache_id(); + if !self.is_store_and_token_initialized(cache_id.as_str()) { + self.get_token_silent().ok()?; + } + + self.token_store.get_stored_token(cache_id.as_str()) + } +} + +impl TokenStore for ConfidentialClientApplication { + fn token_store_provider(&self) -> TokenStoreProvider { + self.token_store.token_store_provider() + } + + fn is_stored_token_initialized(&self, id: &str) -> bool { + self.token_store.is_stored_token_initialized(id) + } + + fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { + self.token_store.get_stored_token(id) + } + + fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { + self.token_store.update_stored_token(id, stored_token) + } + + fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { + self.token_store.get_bearer_token_from_store(id) + } + + fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { + self.token_store.get_refresh_token_from_store(id) + } } #[async_trait] impl TokenCredentialExecutor for ConfidentialClientApplication { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_cloud_instance) + fn uri(&mut self) -> AuthorizationResult<Url> { + self.credential.uri() } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -84,63 +223,11 @@ impl TokenCredentialExecutor for ConfidentialClientApplication { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.credential.uri(&azure_cloud_instance)?; - let form = self.credential.form_urlencode()?; - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - let basic_auth = self.credential.basic_auth(); - if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client - .post(uri) - .basic_auth(client_identifier, Some(secret)) - // Reqwest adds these automatically but this is here in case that changes. - .headers(headers) - .form(&form) - .send()?) - } else { - Ok(http_client.post(uri).headers(headers).form(&form).send()?) - } + self.credential.execute() } async fn execute_async(&mut self) -> AuthExecutionResult<Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.credential.uri(&azure_cloud_instance)?; - let form = self.credential.form_urlencode()?; - let basic_auth = self.credential.basic_auth(); - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - if let Some((client_identifier, secret)) = basic_auth { - Ok(self - .http_client - .post(uri) - // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 - .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) - .send() - .await?) - } else { - Ok(self - .http_client - .post(uri) - .headers(headers) - .form(&form) - .send() - .await?) - } + self.credential.execute_async().await } } @@ -180,6 +267,12 @@ impl From<OpenIdCredential> for ConfidentialClientApplication { } } +impl From<UnInitializedCredentialExecutor> for ConfidentialClientApplication { + fn from(value: UnInitializedCredentialExecutor) -> Self { + ConfidentialClientApplication::credential(value) + } +} + #[cfg(test)] mod test { use super::*; @@ -187,21 +280,18 @@ mod test { #[test] fn confidential_client_new() { - let credential = AuthorizationCodeCredential::builder( - Uuid::new_v4().to_string(), - "ALDSKFJLKERLKJALSDKJF2209LAKJGFL", - ) - .with_client_secret("CLDIE3F") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - let mut confidential_client = credential; - let credential_uri = confidential_client - .credential - .uri(&AzureCloudInstance::AzurePublic) - .unwrap(); + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + let credential_uri = confidential_client.credential.uri().unwrap(); assert_eq!( "https://login.microsoftonline.com/common/oauth2/v2.0/token", @@ -210,25 +300,116 @@ mod test { } #[test] - fn confidential_client_tenant() { - let mut confidential_client = AuthorizationCodeCredential::builder( - Uuid::new_v4().to_string(), - "ALDSKFJLKERLKJALSDKJF2209LAKJGFL", - ) - .with_client_id("bb301aaa-1201-4259-a230923fds32") - .with_client_secret("CLDIE3F") - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .with_authority(Authority::Consumers) - .build(); - let credential_uri = confidential_client - .credential - .uri(&AzureCloudInstance::AzurePublic) - .unwrap(); + fn confidential_client_authority_tenant() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_tenant("tenant") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + let credential_uri = confidential_client.credential.uri().unwrap(); + + assert_eq!( + "https://login.microsoftonline.com/tenant/oauth2/v2.0/token", + credential_uri.as_str() + ); + } + + #[test] + fn confidential_client_authority_consumers() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_authority(Authority::Consumers) + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + let credential_uri = confidential_client.credential.uri().unwrap(); assert_eq!( "https://login.microsoftonline.com/consumers/oauth2/v2.0/token", credential_uri.as_str() ); } + + #[test] + fn in_memory_token_store_init() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + client_id_string, + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent().unwrap(), + "token".to_string() + ) + } + + #[tokio::test] + async fn in_memory_token_store_init_async() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + client_id_string, + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent_async().await.unwrap(), + "token".to_string() + ) + } + + #[tokio::test] + async fn in_memory_token_store_tenant_and_client_cache_id() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_tenant("tenant-id") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + format!("{},{}", "tenant-id", client_id_string), + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent_async().await.unwrap(), + "token".to_string() + ) + } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 954ba0df..4a2f4ab0 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -83,9 +83,10 @@ impl DeviceCodeCredential { } impl TokenCredentialExecutor for DeviceCodeCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority_device_code(azure_cloud_instance, &self.app_config.authority); + .authority_device_code(&azure_cloud_instance, &self.authority()); if self.device_code.is_none() && self.refresh_token.is_none() { let uri = self diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 0e674586..0b77e745 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -18,6 +18,7 @@ const AZURE_CLIENT_SECRET: &str = "AZURE_CLIENT_SECRET"; const AZURE_USERNAME: &str = "AZURE_USERNAME"; const AZURE_PASSWORD: &str = "AZURE_PASSWORD"; +#[derive(Clone)] pub struct EnvironmentCredential { pub credential: Box<dyn TokenCredentialExecutor + Send>, } @@ -127,7 +128,7 @@ impl EnvironmentCredential { impl AuthorizationSerializer for EnvironmentCredential { fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_cloud_instance) + self.credential.uri() } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -136,8 +137,8 @@ impl AuthorizationSerializer for EnvironmentCredential { } impl TokenCredentialExecutor for EnvironmentCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_cloud_instance) + fn uri(&mut self) -> AuthorizationResult<Url> { + self.credential.uri() } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index d4d84428..20ffabe2 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -9,7 +9,6 @@ mod as_query; mod auth_code_authorization_url_parameters; mod authorization_code_certificate_credential; mod authorization_code_credential; -mod client_application; mod client_assertion_credential; mod client_certificate_credential; mod client_credentials_authorization_url; @@ -39,7 +38,6 @@ pub use as_query::*; pub use auth_code_authorization_url_parameters::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; -pub use client_application::*; pub use client_builder_impl::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index abae75bd..d3d86796 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -116,7 +116,6 @@ impl OpenIdAuthorizationUrl { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - token_store: Default::default(), }, response_type: BTreeSet::new(), response_mode: None, diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 9eae8a77..c0868f5f 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -76,7 +76,6 @@ impl OpenIdCredential { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - token_store: Default::default(), }, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, @@ -108,9 +107,10 @@ impl OpenIdCredential { #[async_trait] impl TokenCredentialExecutor for OpenIdCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.app_config.authority); + .authority(&azure_cloud_instance, &self.app_config.authority); if self.refresh_token.is_none() { let uri = self diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 40e0adf8..8d0358d8 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -4,8 +4,13 @@ use crate::identity::{ Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredentialExecutor, }; +use crate::oauth::UnInitializedCredentialExecutor; use async_trait::async_trait; -use graph_error::{AuthExecutionResult, AuthorizationResult}; +use graph_error::{AuthExecutionResult, AuthorizationResult, AF}; +use graph_extensions::cache::{ + InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, UnInitializedTokenStore, +}; +use graph_extensions::token::{ClientApplication, ClientApplicationType, MsalToken}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; @@ -18,9 +23,11 @@ use uuid::Uuid; /// installed native application or a web browser-based application), and incapable of /// secure client authentication via any other means. /// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 +#[derive(Clone)] pub struct PublicClientApplication { http_client: reqwest::Client, credential: Box<dyn TokenCredentialExecutor + Send>, + token_store: Box<dyn TokenStore + Send>, } impl PublicClientApplication { @@ -42,18 +49,120 @@ impl PublicClientApplication { .build() .unwrap(), credential: Box::new(credential), + token_store: Box::new(UnInitializedTokenStore), } } pub fn builder(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { PublicClientApplicationBuilder::new(client_id.as_ref()) } + + pub fn with_in_memory_token_store(&mut self) { + self.token_store = Box::new(InMemoryCredentialStore::new( + self.app_config().cache_id(), + StoredToken::UnInitialized, + )); + } +} + +#[async_trait] +impl ClientApplication for PublicClientApplication { + fn client_application_type(&self) -> ClientApplicationType { + ClientApplicationType::ConfidentialClientApplication + } + + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + let cache_id = self.app_config().cache_id(); + if self.is_store_and_token_initialized(cache_id.as_str()) { + return Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error getting token from store - please report issue", + ))? + .clone()); + } + + if !self.is_token_store_initialized() { + self.with_in_memory_token_store(); + } + + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); + Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error initializing token store - please report issue", + ))? + .clone()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + let cache_id = self.app_config().cache_id(); + if self.is_store_and_token_initialized(cache_id.as_str()) { + return Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error getting token from store - please report issue", + ))? + .clone()); + } + + if !self.is_token_store_initialized() { + self.with_in_memory_token_store(); + } + + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); + Ok(self + .get_bearer_token_from_store(cache_id.as_str()) + .ok_or(AF::unknown( + "Unknown error initializing token store - please report issue", + ))? + .clone()) + } + + fn get_stored_application_token(&mut self) -> Option<&StoredToken> { + let cache_id = self.app_config().cache_id(); + if !self.is_store_and_token_initialized(cache_id.as_str()) { + self.get_token_silent().ok()?; + } + + self.token_store.get_stored_token(cache_id.as_str()) + } +} + +impl TokenStore for PublicClientApplication { + fn token_store_provider(&self) -> TokenStoreProvider { + self.token_store.token_store_provider() + } + + fn is_stored_token_initialized(&self, id: &str) -> bool { + self.token_store.is_stored_token_initialized(id) + } + + fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { + self.token_store.get_stored_token(id) + } + + fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { + self.token_store.update_stored_token(id, stored_token) + } + + fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { + self.token_store.get_bearer_token_from_store(id) + } + + fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { + self.token_store.get_refresh_token_from_store(id) + } } #[async_trait] impl TokenCredentialExecutor for PublicClientApplication { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { - self.credential.uri(azure_cloud_instance) + fn uri(&mut self) -> AuthorizationResult<Url> { + self.credential.uri() } fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { @@ -77,8 +186,7 @@ impl TokenCredentialExecutor for PublicClientApplication { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.credential.uri(&azure_cloud_instance)?; + let uri = self.credential.uri()?; let form = self.credential.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() @@ -105,8 +213,7 @@ impl TokenCredentialExecutor for PublicClientApplication { } async fn execute_async(&mut self) -> AuthExecutionResult<Response> { - let azure_cloud_instance = self.credential.azure_cloud_instance(); - let uri = self.credential.uri(&azure_cloud_instance)?; + let uri = self.credential.uri()?; let form = self.credential.form_urlencode()?; let basic_auth = self.credential.basic_auth(); @@ -149,3 +256,9 @@ impl From<DeviceCodeCredential> for PublicClientApplication { PublicClientApplication::credential(value) } } + +impl From<UnInitializedCredentialExecutor> for PublicClientApplication { + fn from(value: UnInitializedCredentialExecutor) -> Self { + PublicClientApplication::credential(value) + } +} diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index edbc43b2..ebca8773 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -68,9 +68,10 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self) -> AuthorizationResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); self.serializer - .authority(azure_cloud_instance, &self.app_config.authority); + .authority(&azure_cloud_instance, &self.app_config.authority); let uri = self .serializer @@ -139,6 +140,22 @@ impl ResourceOwnerPasswordCredentialBuilder { } } + pub(crate) fn new_with_username_password<T: AsRef<str>>( + username: T, + password: T, + app_config: AppConfig, + ) -> ResourceOwnerPasswordCredentialBuilder { + ResourceOwnerPasswordCredentialBuilder { + credential: ResourceOwnerPasswordCredential { + app_config, + username: username.as_ref().to_owned(), + password: password.as_ref().to_owned(), + scope: vec![], + serializer: Default::default(), + }, + } + } + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { self.credential.app_config.client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 69e267cc..e23fc805 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -2,6 +2,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance}; use async_trait::async_trait; +use dyn_clone::DynClone; use graph_error::{AuthExecutionResult, AuthorizationResult}; use http::header::ACCEPT; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; @@ -11,17 +12,29 @@ use std::collections::HashMap; use url::Url; use uuid::Uuid; -pub struct UserInfoEndpoint { - user_info_endpoint: String, -} +dyn_clone::clone_trait_object!(TokenCredentialExecutor); #[async_trait] -pub trait TokenCredentialExecutor { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url>; +pub trait TokenCredentialExecutor: DynClone { + fn is_initialized(&self) -> bool { + true + } + + fn uri(&mut self) -> AuthorizationResult<Url>; + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; - fn client_id(&self) -> &Uuid; - fn authority(&self) -> Authority; - fn azure_cloud_instance(&self) -> AzureCloudInstance; + + fn client_id(&self) -> &Uuid { + &self.app_config().client_id + } + + fn authority(&self) -> Authority { + self.app_config().authority.clone() + } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config().azure_cloud_instance + } fn basic_auth(&self) -> Option<(String, String)> { None @@ -29,6 +42,14 @@ pub trait TokenCredentialExecutor { fn app_config(&self) -> &AppConfig; + fn extra_header_parameters(&self) -> &HeaderMap { + &self.app_config().extra_header_parameters + } + + fn extra_query_parameters(&self) -> &HashMap<String, String> { + &self.app_config().extra_query_parameters + } + fn openid_configuration_url(&self) -> AuthorizationResult<Url> { Ok(Url::parse( format!( @@ -40,26 +61,6 @@ pub trait TokenCredentialExecutor { )?) } - fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let response = self.get_openid_config()?; - let config: serde_json::Value = response.json()?; - let user_info_endpoint = Url::parse(config["userinfo_endpoint"].as_str().unwrap()).unwrap(); - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - - let response = http_client - .get(user_info_endpoint) - .headers(headers) - .send() - .expect("Error on header"); - - Ok(response) - } - fn get_openid_config(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let open_id_url = self.openid_configuration_url()?; let http_client = reqwest::blocking::ClientBuilder::new() @@ -99,8 +100,7 @@ pub trait TokenCredentialExecutor { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let options = self.azure_cloud_instance(); - let uri = self.uri(&options)?; + let mut uri = self.uri()?; let form = self.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -112,6 +112,25 @@ pub trait TokenCredentialExecutor { HeaderValue::from_static("application/x-www-form-urlencoded"), ); + let extra_headers = self.extra_header_parameters(); + if !extra_headers.is_empty() { + if extra_headers.contains_key(ACCEPT) { + panic!("extra header parameters cannot contain header key ACCEPT") + } + + for (header_name, header_value) in extra_headers.iter() { + headers.insert(header_name, header_value.clone()); + } + } + + let extra_query_params = self.extra_query_parameters(); + if !extra_query_params.is_empty() { + for (key, value) in extra_query_params.iter() { + uri.query_pairs_mut() + .append_pair(key.as_ref(), value.as_ref()); + } + } + let basic_auth = self.basic_auth(); if let Some((client_identifier, secret)) = basic_auth { Ok(http_client @@ -126,8 +145,7 @@ pub trait TokenCredentialExecutor { } async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.uri(&azure_cloud_instance)?; + let mut uri = self.uri()?; let form = self.form_urlencode()?; let http_client = ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -139,6 +157,25 @@ pub trait TokenCredentialExecutor { HeaderValue::from_static("application/x-www-form-urlencoded"), ); + let extra_headers = self.extra_header_parameters(); + if !extra_headers.is_empty() { + if extra_headers.contains_key(ACCEPT) { + panic!("extra header parameters cannot contain header key ACCEPT") + } + + for (header_name, header_value) in extra_headers.iter() { + headers.insert(header_name, header_value.clone()); + } + } + + let extra_query_params = self.extra_query_parameters(); + if !extra_query_params.is_empty() { + for (key, value) in extra_query_params.iter() { + uri.query_pairs_mut() + .append_pair(key.as_ref(), value.as_ref()); + } + } + let basic_auth = self.basic_auth(); if let Some((client_identifier, secret)) = basic_auth { Ok(http_client @@ -159,6 +196,27 @@ pub trait TokenCredentialExecutor { } } +#[derive(Clone)] +pub struct UnInitializedCredentialExecutor; + +impl TokenCredentialExecutor for UnInitializedCredentialExecutor { + fn is_initialized(&self) -> bool { + false + } + + fn uri(&mut self) -> AuthorizationResult<Url> { + panic!("TokenCredentialExecutor is UnInitialized"); + } + + fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + panic!("TokenCredentialExecutor is UnInitialized"); + } + + fn app_config(&self) -> &AppConfig { + panic!("TokenCredentialExecutor is UnInitialized"); + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index c06badc5..728fcecd 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -3,18 +3,18 @@ mod application_options; mod authority; mod authorization_query_response; mod authorization_serializer; -mod credential_store; mod credentials; mod device_code; +mod token_validator; pub use allowed_host_validator::*; pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; pub use authorization_serializer::*; -pub use credential_store::*; pub use credentials::*; pub use device_code::*; +pub use token_validator::*; #[cfg(feature = "openssl")] pub use openssl::{ diff --git a/graph-oauth/src/identity/token_validator.rs b/graph-oauth/src/identity/token_validator.rs new file mode 100644 index 00000000..1c24d21d --- /dev/null +++ b/graph-oauth/src/identity/token_validator.rs @@ -0,0 +1,15 @@ +#[derive(Clone, Default)] +pub struct TokenValidator { + application_id: Option<String>, +} + +impl TokenValidator { + pub fn builder() -> TokenValidator { + TokenValidator::default() + } + + pub fn with_application_id(&mut self, aud: impl AsRef<str>) -> &mut Self { + self.application_id = Some(aud.as_ref().to_owned()); + self + } +} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 8de4e4f1..71e2768d 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -27,22 +27,22 @@ //! - [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) //! //! -//! # Example +//! # Example ConfidentialClientApplication Authorization Code Flow //! ```rust //! use url::Url; //! use graph_error::AuthorizationResult; //! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! //! pub fn authorization_url(client_id: &str) -> AuthorizationResult<Url> { -//! AuthorizationCodeCredential::authorization_url_builder() -//! .with_client_id(client_id) +//! let auth_url_parameters = ConfidentialClientApplication::builder(client_id) +//! .authorization_code_url_builder() //! .with_redirect_uri("http://localhost:8000/redirect") //! .with_scope(vec!["user.read"]) -//! .url() +//! .build(); //! } //! //! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication> { -//! Ok(AuthorizationCodeCredential::builder(client_id) +//! Ok(ConfidentialClientApplication::builder(client_id) //! .with_authorization_code(authorization_code) //! .with_client_secret(client_secret) //! .with_scope(vec!["user.read"]) @@ -59,19 +59,15 @@ extern crate serde; extern crate log; extern crate pretty_env_logger; -mod access_token; mod auth; mod discovery; mod grants; -mod id_token; pub mod jwt; mod oauth_error; pub mod identity; -pub mod web; pub mod oauth { - pub use crate::access_token::MsalTokenResponse; pub use crate::auth::GrantSelector; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; @@ -80,8 +76,8 @@ pub mod oauth { pub use crate::discovery::well_known; pub use crate::grants::GrantRequest; pub use crate::grants::GrantType; - pub use crate::id_token::IdToken; pub use crate::identity::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; + pub use graph_extensions::token::{IdToken, MsalToken}; } diff --git a/src/client/graph.rs b/src/client/graph.rs index 9647fc83..c38cc4f4 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AllowedHostValidator, HostValidator, MsalTokenResponse, OAuthSerializer}; +use crate::oauth::{AllowedHostValidator, HostValidator, MsalToken, OAuthSerializer}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -65,6 +65,7 @@ use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_error::GraphFailure; +use graph_extensions::token::ClientApplication; use graph_http::api_impl::GraphClientConfiguration; use lazy_static::lazy_static; use std::convert::TryFrom; @@ -83,9 +84,17 @@ pub struct Graph { } impl Graph { - pub fn new(access_token: &str) -> Graph { + pub fn new<AT: ToString>(access_token: AT) -> Graph { Graph { - client: Client::new(access_token), + client: Client::new(BearerToken(access_token.to_string())), + endpoint: PARSED_GRAPH_URL.clone(), + allowed_host_validator: AllowedHostValidator::default(), + } + } + + pub fn from_client_app<CA: ClientApplication + 'static>(client_app: CA) -> Graph { + Graph { + client: Client::new(client_app), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), } @@ -516,19 +525,19 @@ impl Graph { impl From<&str> for Graph { fn from(token: &str) -> Self { - Graph::new(token) + Graph::from_client_app(BearerToken(token.into())) } } impl From<String> for Graph { fn from(token: String) -> Self { - Graph::new(token.as_str()) + Graph::from_client_app(BearerToken(token)) } } -impl From<&MsalTokenResponse> for Graph { - fn from(token: &MsalTokenResponse) -> Self { - Graph::new(token.access_token.as_str()) +impl From<&MsalToken> for Graph { + fn from(token: &MsalToken) -> Self { + Graph::from_client_app(BearerToken(token.access_token.clone())) } } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 42bf208d..c8dda868 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, MsalTokenResponse, - ResourceOwnerPasswordCredential, TokenCredentialExecutor, + ConfidentialClientApplication, MsalToken, ResourceOwnerPasswordCredential, + TokenCredentialExecutor, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -12,6 +12,7 @@ use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; +use graph_http::api_impl::BearerToken; use std::sync::Mutex; // static mutex's that are used for preventing test failures @@ -121,14 +122,11 @@ impl OAuthTestCredentials { } } - fn client_credentials(self) -> ClientSecretCredential { - let mut builder = ConfidentialClientApplication::builder(self.client_id.as_str()) - .with_client_secret(self.client_secret.as_str()); - - builder + fn client_credentials(self) -> ConfidentialClientApplication { + ConfidentialClientApplication::builder(self.client_id.as_str()) + .with_client_secret(self.client_secret.as_str()) .with_tenant(self.tenant.as_str()) - .with_scope(vec!["https://graph.microsoft.com/.default"]) - .credential() + .build() } fn resource_owner_password_credential(self) -> ResourceOwnerPasswordCredential { @@ -150,13 +148,13 @@ pub enum OAuthTestClient { } impl OAuthTestClient { - fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, MsalTokenResponse)> { + fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, MsalToken)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); if let Ok(response) = credential.execute() { - let token: MsalTokenResponse = response.json().unwrap(); + let token: MsalToken = response.json().unwrap(); Some((user_id, token)) } else { None @@ -165,7 +163,7 @@ impl OAuthTestClient { OAuthTestClient::ResourceOwnerPasswordCredentials => { let mut credential = creds.resource_owner_password_credential(); if let Ok(response) = credential.execute() { - let token: MsalTokenResponse = response.json().unwrap(); + let token: MsalToken = response.json().unwrap(); Some((user_id, token)) } else { None @@ -175,21 +173,24 @@ impl OAuthTestClient { } } - pub fn get_client_credentials(&self, creds: OAuthTestCredentials) -> ClientSecretCredential { + pub fn get_client_credentials( + &self, + creds: OAuthTestCredentials, + ) -> ConfidentialClientApplication { creds.client_credentials() } async fn get_access_token_async( &self, creds: OAuthTestCredentials, - ) -> Option<(String, MsalTokenResponse)> { + ) -> Option<(String, MsalToken)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); match credential.execute_async().await { Ok(response) => { - let token: MsalTokenResponse = response.json().await.unwrap(); + let token: MsalToken = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -199,7 +200,7 @@ impl OAuthTestClient { let mut credential = creds.resource_owner_password_credential(); match credential.execute_async().await { Ok(response) => { - let token: MsalTokenResponse = response.json().await.unwrap(); + let token: MsalToken = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -209,7 +210,7 @@ impl OAuthTestClient { } } - pub fn request_access_token(&self) -> Option<(String, MsalTokenResponse)> { + pub fn request_access_token(&self) -> Option<(String, MsalToken)> { if Environment::is_local() || Environment::is_travis() { let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); self.get_access_token(map.get(self).unwrap()) @@ -224,7 +225,7 @@ impl OAuthTestClient { } } - pub async fn request_access_token_async(&self) -> Option<(String, MsalTokenResponse)> { + pub async fn request_access_token_async(&self) -> Option<(String, MsalToken)> { if Environment::is_local() || Environment::is_travis() { let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); self.get_access_token_async(map.get(self).unwrap()).await @@ -258,7 +259,7 @@ impl OAuthTestClient { let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token(credentials) { - Some((id, Graph::new(token.access_token.as_str()))) + Some((id, Graph::new(token.access_token))) } else { None } @@ -266,7 +267,7 @@ impl OAuthTestClient { pub fn client_credentials_by_rid( resource_identity: ResourceIdentity, - ) -> Option<ClientSecretCredential> { + ) -> Option<ConfidentialClientApplication> { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; @@ -280,7 +281,7 @@ impl OAuthTestClient { let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token_async(credentials).await { - Some((id, Graph::new(token.access_token.as_str()))) + Some((id, Graph::from_client_app(BearerToken(token.access_token)))) } else { None } @@ -288,7 +289,7 @@ impl OAuthTestClient { pub fn graph(&self) -> Option<(String, Graph)> { if let Some((id, token)) = self.request_access_token() { - Some((id, Graph::new(token.access_token.as_str()))) + Some((id, Graph::from_client_app(BearerToken(token.access_token)))) } else { None } @@ -296,13 +297,13 @@ impl OAuthTestClient { pub async fn graph_async(&self) -> Option<(String, Graph)> { if let Some((id, token)) = self.request_access_token_async().await { - Some((id, Graph::new(token.access_token.as_str()))) + Some((id, Graph::from_client_app(BearerToken(token.access_token)))) } else { None } } - pub fn token(resource_identity: ResourceIdentity) -> Option<MsalTokenResponse> { + pub fn token(resource_identity: ResourceIdentity) -> Option<MsalToken> { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, _credentials) = client.default_client()?; diff --git a/tests/access_token_tests.rs b/tests/access_token_tests.rs deleted file mode 100644 index 009fae81..00000000 --- a/tests/access_token_tests.rs +++ /dev/null @@ -1,45 +0,0 @@ -use graph_oauth::oauth::MsalTokenResponse; -use std::thread; -use std::time::Duration; - -#[test] -fn is_expired_test() { - let mut access_token = MsalTokenResponse::default(); - access_token.set_expires_in(1); - thread::sleep(Duration::from_secs(3)); - assert!(access_token.is_expired()); - let mut access_token = MsalTokenResponse::default(); - access_token.set_expires_in(10); - thread::sleep(Duration::from_secs(4)); - assert!(!access_token.is_expired()); -} - -pub const ACCESS_TOKEN_INT: &str = r#"{ - "access_token": "fasdfasdfasfdasdfasfsdf", - "token_type": "Bearer", - "expires_in": 65874, - "scope": null, - "refresh_token": null, - "user_id": "santa@north.pole.com", - "id_token": "789aasdf-asdf", - "state": null, - "timestamp": "2020-10-27T16:31:38.788098400Z" -}"#; - -pub const ACCESS_TOKEN_STRING: &str = r#"{ - "access_token": "fasdfasdfasfdasdfasfsdf", - "token_type": "Bearer", - "expires_in": "65874", - "scope": null, - "refresh_token": null, - "user_id": "helpers@north.pole.com", - "id_token": "789aasdf-asdf", - "state": null, - "timestamp": "2020-10-27T16:31:38.788098400Z" -}"#; - -#[test] -pub fn test_deserialize() { - let _token: MsalTokenResponse = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); - let _token: MsalTokenResponse = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); -} diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs index 445972db..efd8be8f 100644 --- a/tests/grants_authorization_code.rs +++ b/tests/grants_authorization_code.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, MsalTokenResponse, OAuthSerializer}; +use graph_rs_sdk::oauth::{GrantRequest, MsalToken, OAuthSerializer}; use test_tools::oauth::OAuthTestTool; use url::{Host, Url}; @@ -68,9 +68,13 @@ fn refresh_token_uri() { .add_scope("Fall.Down") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut access_token = - MsalTokenResponse::new("access_token", 3600, "Read.Write Fall.Down", "asfasf"); - access_token.set_refresh_token("32LKLASDKJ"); + let mut access_token = MsalToken::new( + "access_token", + 3600, + "asfasf", + vec!["Read.Write", "Fall.Down"], + ); + access_token.with_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); let body = oauth @@ -78,7 +82,7 @@ fn refresh_token_uri() { .unwrap(); let test_url = "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&refresh_token=32LKLASDKJ&grant_type=refresh_token&scope=Fall.Down+Read.Write"; - assert_eq!(test_url, body); + assert_eq!(test_url, access_token.get_token); } #[test] diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs index 8b5c9a7f..48ff2e08 100644 --- a/tests/grants_code_flow.rs +++ b/tests/grants_code_flow.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, MsalTokenResponse, OAuthSerializer}; +use graph_rs_sdk::oauth::{GrantRequest, MsalToken, OAuthSerializer}; #[test] fn sign_in_code_url() { @@ -49,12 +49,12 @@ fn access_token() { .authorization_url("https://www.example.com/token") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut builder = MsalTokenResponse::default(); + let mut builder = MsalToken::default(); builder - .set_token_type("token") - .set_bearer_token("access_token") - .set_expires_in(3600) - .set_scope("scope"); + .with_token_type("token") + .with_access_token("access_token") + .with_expires_in(3600) + .with_scope(vec!["scope"]); let code_body = oauth .encode_uri(GrantType::CodeFlow, GrantRequest::AccessToken) @@ -75,8 +75,8 @@ fn refresh_token() { .authorization_url("https://www.example.com/token") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write", "asfasf"); - access_token.set_refresh_token("32LKLASDKJ"); + let mut access_token = MsalToken::new("access_token", 3600, "asfasf", vec!["Read.Write"]); + access_token.with_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); let body = oauth @@ -100,8 +100,8 @@ fn get_refresh_token() { .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); - let mut access_token = MsalTokenResponse::new("access_token", 3600, "Read.Write", "asfasf"); - access_token.set_refresh_token("32LKLASDKJ"); + let mut access_token = MsalToken::new("Bearer", 3600, "token", vec!["User.Read"]); + access_token.with_refresh_token("32LKLASDKJ"); oauth.access_token(access_token); assert_eq!("32LKLASDKJ", oauth.get_refresh_token().unwrap()); From b74b756e112f56e802a1970bb93c81151f291605 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 30 Sep 2023 13:07:32 -0400 Subject: [PATCH 041/118] Automatic token watcher impl --- .../auth_code_grant/auth_code_grant_pkce.rs | 6 +- .../auth_code_grant/auth_code_grant_secret.rs | 16 ++ .../client_credentials_admin_consent.rs | 20 +-- examples/oauth/main.rs | 6 +- .../openid_connect_form_post.rs | 5 +- examples/oauth/signing_keys.rs | 44 ------ examples/oauth_authorization_url/main.rs | 4 +- .../oauth_authorization_url/openid_connect.rs | 28 +++- graph-error/src/authorization_failure.rs | 8 +- graph-error/src/lib.rs | 2 +- graph-extensions/Cargo.toml | 4 +- graph-extensions/src/cache/mod.rs | 8 +- .../src/cache/token_watch_task.rs | 21 +++ graph-extensions/src/crypto/mod.rs | 18 +++ graph-extensions/src/crypto/pkce.rs | 143 ++++++++++++++++++ graph-extensions/src/lib.rs | 1 + .../src/token/client_application.rs | 2 - graph-extensions/src/token/msal_token.rs | 22 +-- .../src/web/interactive_web_view.rs | 7 +- graph-http/src/client.rs | 60 +++++--- graph-oauth/src/auth.rs | 13 +- graph-oauth/src/discovery/graph_discovery.rs | 119 +-------------- graph-oauth/src/discovery/mod.rs | 1 - graph-oauth/src/discovery/well_known.rs | 26 ---- .../src/identity/authorization_serializer.rs | 10 +- .../src/identity/credentials/app_config.rs | 2 +- .../credentials/application_builder.rs | 21 +-- ...ters.rs => auth_code_authorization_url.rs} | 56 ++++--- ...thorization_code_certificate_credential.rs | 8 +- .../authorization_code_credential.rs | 19 +-- .../client_assertion_credential.rs | 6 +- .../client_certificate_credential.rs | 6 +- .../client_credentials_authorization_url.rs | 23 +-- .../credentials/client_secret_credential.rs | 6 +- .../confidential_client_application.rs | 57 +++++-- .../src/identity/credentials/crypto.rs | 37 ----- .../credentials/device_code_credential.rs | 6 +- .../credentials/environment_credential.rs | 20 +-- .../credentials/implicit_credential.rs | 18 +-- .../legacy/code_flow_authorization_url.rs | 4 +- .../legacy/code_flow_credential.rs | 6 +- .../legacy/token_flow_authorization_url.rs | 4 +- graph-oauth/src/identity/credentials/mod.rs | 9 +- .../credentials/open_id_authorization_url.rs | 107 ++++++++----- .../credentials/open_id_credential.rs | 29 ++-- .../proof_key_for_code_exchange.rs | 72 --------- .../credentials/public_client_application.rs | 10 +- .../resource_owner_password_credential.rs | 8 +- .../credentials/token_credential_executor.rs | 12 +- graph-oauth/src/lib.rs | 11 +- test-tools/src/oauth_request.rs | 3 + tests/discovery_tests.rs | 104 ------------- tests/grants_authorization_code.rs | 120 --------------- tests/todo_tasks_request.rs | 1 - 54 files changed, 580 insertions(+), 799 deletions(-) delete mode 100644 examples/oauth/signing_keys.rs create mode 100644 graph-extensions/src/cache/token_watch_task.rs create mode 100644 graph-extensions/src/crypto/mod.rs create mode 100644 graph-extensions/src/crypto/pkce.rs delete mode 100644 graph-oauth/src/discovery/well_known.rs rename graph-oauth/src/identity/credentials/{auth_code_authorization_url_parameters.rs => auth_code_authorization_url.rs} (93%) delete mode 100644 graph-oauth/src/identity/credentials/crypto.rs delete mode 100644 graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs delete mode 100644 tests/discovery_tests.rs delete mode 100644 tests/grants_authorization_code.rs diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index 061cd9f4..822e2a5b 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,8 +1,8 @@ use graph_oauth::identity::ResponseType; -use graph_rs_sdk::error::AuthorizationResult; +use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalToken, ProofKeyForCodeExchange, TokenCredentialExecutor, TokenRequest, + GenPkce, MsalToken, ProofKeyCodeExchange, TokenCredentialExecutor, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; @@ -13,7 +13,7 @@ static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; // You can also pass your own values for PKCE instead of automatic generation by // calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) lazy_static! { - static ref PKCE: ProofKeyForCodeExchange = ProofKeyForCodeExchange::generate().unwrap(); + static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap(); } #[derive(Default, Debug, Clone, Serialize, Deserialize)] diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index 804c61c1..ffe71a28 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -53,6 +53,22 @@ pub async fn start_server_main() { warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; } +/// # Use the access code to build Confidential Client Application +/// +/// ```rust +/// fn main() { +/// use graph_rs_sdk::oauth::ConfidentialClientApplication; +/// +/// // Set the access code and request an access token. +/// // Callers should handle the Result from requesting an access token +/// // in case of an error here. +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); +/// } +/// ``` async fn handle_redirect( code_option: Option<AccessCode>, ) -> Result<Box<dyn warp::Reply>, warp::Rejection> { diff --git a/examples/oauth/client_credentials/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/client_credentials_admin_consent.rs index d4abc563..6b3ecc28 100644 --- a/examples/oauth/client_credentials/client_credentials_admin_consent.rs +++ b/examples/oauth/client_credentials/client_credentials_admin_consent.rs @@ -1,14 +1,16 @@ +// ADMIN CONSENT +// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, +// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent +// only has to be done once for a user. After admin consent is given, the oauth client can be +// used to continue getting new access tokens programmatically. + +// OVERVIEW // Microsoft Client Credentials: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow // You can use the OAuth 2.0 client credentials grant specified in RFC 6749, // sometimes called two-legged OAuth, to access web-hosted resources by using the // identity of an application. This type of grant is commonly used for server-to-server // interactions that must run in the background, without immediate interaction with a user. // These types of applications are often referred to as daemons or service accounts. -// -// This OAuth flow example requires signing in as an administrator for Azure, known as admin consent, -// to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent -// only has to be done once for a user. After admin consent is given, the oauth client can be -// used to continue getting new access tokens programmatically. // This example shows getting the URL for the one time admin consent required // for the client credentials flow. @@ -18,7 +20,7 @@ // used to get access tokens programmatically without any consent by a user // or admin. See examples/client_credentials.rs -use graph_rs_sdk::error::AuthorizationResult; +use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrl; use warp::Filter; @@ -26,8 +28,8 @@ use warp::Filter; static CLIENT_ID: &str = "<CLIENT_ID>"; static REDIRECT_URI: &str = "http://localhost:8000/redirect"; -// Paste the URL into a browser and have the admin sign in and approve the admin consent. -fn get_admin_consent_url() -> AuthorizationResult<url::Url> { +// Paste the URL into a browser and log in to approve the admin consent. +fn get_admin_consent_url() -> IdentityResult<url::Url> { let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI)?; authorization_credential.url() } @@ -35,7 +37,7 @@ fn get_admin_consent_url() -> AuthorizationResult<url::Url> { // OR use the builder: // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. -fn get_admin_consent_url_from_builder() -> AuthorizationResult<url::Url> { +fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { let authorization_credential = ClientCredentialsAuthorizationUrl::builder(CLIENT_ID) .with_redirect_uri(REDIRECT_URI)? .with_state("123") diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index f843784b..a5079741 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -21,13 +21,13 @@ mod device_code; mod environment_credential; mod is_access_token_expired; mod openid_connect; -mod signing_keys; use crate::is_access_token_expired::is_access_token_expired; +use graph_extensions::crypto::GenPkce; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, MsalToken, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, MsalToken, ProofKeyCodeExchange, PublicClientApplication, TokenCredentialExecutor, TokenRequest, }; @@ -57,7 +57,7 @@ fn main() { // Authorization Code Grant async fn auth_code_grant(authorization_code: &str) { - let pkce = ProofKeyForCodeExchange::generate().unwrap(); + let pkce = ProofKeyCodeExchange::oneshot().unwrap(); let credential = AuthorizationCodeCredential::builder("CLIENT_ID", "CLIENT_SECRET", authorization_code) diff --git a/examples/oauth/openid_connect/openid_connect_form_post.rs b/examples/oauth/openid_connect/openid_connect_form_post.rs index b3a3c566..f4861c46 100644 --- a/examples/oauth/openid_connect/openid_connect_form_post.rs +++ b/examples/oauth/openid_connect/openid_connect_form_post.rs @@ -34,8 +34,7 @@ static TENANT_ID: &str = ""; static REDIRECT_URI: &str = "http://localhost:8000/redirect"; fn openid_authorization_url() -> anyhow::Result<Url> { - Ok(OpenIdCredential::authorization_url_builder()? - .with_client_id(CLIENT_ID) + Ok(OpenIdCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT_ID) //.with_default_scope()? .with_redirect_uri(REDIRECT_URI)? @@ -43,7 +42,7 @@ fn openid_authorization_url() -> anyhow::Result<Url> { .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) .with_state("1234") - .extend_scope(vec!["User.Read", "User.ReadWrite"]) + .with_scope(vec!["User.Read", "User.ReadWrite"]) .build() .url()?) } diff --git a/examples/oauth/signing_keys.rs b/examples/oauth/signing_keys.rs deleted file mode 100644 index 1716be25..00000000 --- a/examples/oauth/signing_keys.rs +++ /dev/null @@ -1,44 +0,0 @@ -use graph_rs_sdk::oauth::graph_discovery::{ - GraphDiscovery, MicrosoftSigningKeysV1, MicrosoftSigningKeysV2, -}; -use graph_rs_sdk::oauth::OAuthSerializer; - -fn get_signing_keys() { - // Lists info such as the authorization and token urls, jwks uri, and response types supported. - let signing_keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.signing_keys().unwrap(); - println!("{signing_keys:#?}"); - - let signing_keys2: MicrosoftSigningKeysV2 = GraphDiscovery::V2.signing_keys().unwrap(); - println!("{signing_keys2:#?}"); - - // You can also create an OAuth instance from the signing keys. OAuth will use - // parameters such as the authorization and token urls. This can save some - // configuration time when setting values for OAuth. However, this will disregard - // all other parameters for the MicrosoftSigningKeys. Use this if you do not - // need the other values. - let _oauth: OAuthSerializer = GraphDiscovery::V1.oauth().unwrap(); -} - -fn tenant_discovery() { - let _oauth: OAuthSerializer = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) - .oauth() - .unwrap(); -} - -// Using async -async fn async_keys_discovery() { - let signing_keys: MicrosoftSigningKeysV1 = - GraphDiscovery::V1.async_signing_keys().await.unwrap(); - println!("{signing_keys:#?}"); - - let signing_keys2: MicrosoftSigningKeysV2 = - GraphDiscovery::V2.async_signing_keys().await.unwrap(); - println!("{signing_keys2:#?}"); -} - -async fn async_tenant_discovery() { - let _oauth: OAuthSerializer = GraphDiscovery::Tenant("<YOUR_TENANT_ID>".into()) - .async_oauth() - .await - .unwrap(); -} diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index ccdff579..5619eb86 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -14,7 +14,7 @@ mod openid_connect; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, MsalToken, ProofKeyForCodeExchange, PublicClientApplication, + DeviceCodeCredential, GenPkce, MsalToken, ProofKeyCodeExchange, PublicClientApplication, TokenCredentialExecutor, TokenRequest, }; @@ -53,7 +53,7 @@ pub fn auth_code_grant_authorization() { // url and query needed to get an authorization code and opens the default system // web browser to this Url. fn auth_code_grant_pkce_authorization() { - let pkce = ProofKeyForCodeExchange::generate().unwrap(); + let pkce = ProofKeyCodeExchange::oneshot().unwrap(); let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_scope(vec![SCOPE]) diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index 87503308..a4faaa90 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -1,4 +1,5 @@ -use graph_oauth::identity::OpenIdCredential; +use graph_error::IdentityResult; +use graph_oauth::identity::{ConfidentialClientApplication, OpenIdCredential}; use url::Url; // The authorization request is the initial request to sign in where the user @@ -17,12 +18,27 @@ fn open_id_authorization_url( tenant: &str, redirect_uri: &str, scope: Vec<&str>, -) -> anyhow::Result<Url> { - Ok(OpenIdCredential::authorization_url_builder()? - .with_client_id(client_id) +) -> IdentityResult<Url> { + ConfidentialClientApplication::builder(client_id) + .openid_authorization_url_builder() .with_tenant(tenant) .with_redirect_uri(redirect_uri)? - .extend_scope(scope) + .with_scope(scope) .build() - .url()?) + .url() +} + +// Same as above +fn open_id_authorization_url2( + client_id: &str, + tenant: &str, + redirect_uri: &str, + scope: Vec<&str>, +) -> IdentityResult<Url> { + OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant) + .with_redirect_uri(redirect_uri)? + .with_scope(scope) + .build() + .url() } diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 531434b8..9f6a7909 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,4 +1,4 @@ -use crate::AuthorizationResult; +use crate::IdentityResult; use tokio::sync::mpsc::error::SendTimeoutError; pub type AF = AuthorizationFailure; @@ -26,7 +26,7 @@ impl AuthorizationFailure { AuthorizationFailure::Unknown(value.to_string()) } - pub fn unknown_result<T: ToString>(value: T) -> AuthorizationResult<AuthorizationFailure> { + pub fn unknown_result<T: ToString>(value: T) -> IdentityResult<AuthorizationFailure> { Err(AuthorizationFailure::Unknown(value.to_string())) } @@ -37,7 +37,7 @@ impl AuthorizationFailure { } } - pub fn result<U>(name: impl AsRef<str>) -> AuthorizationResult<U> { + pub fn result<U>(name: impl AsRef<str>) -> IdentityResult<U> { Err(AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), message: None, @@ -76,7 +76,7 @@ impl AuthorizationFailure { Err(AuthorizationFailure::UrlParseError(url_parse_error)) } - pub fn condition(cond: bool, name: &str, msg: &str) -> AuthorizationResult<()> { + pub fn condition(cond: bool, name: &str, msg: &str) -> IdentityResult<()> { if cond { AF::msg_result(name, msg) } else { diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index 2d11c18d..63a8bea9 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -16,6 +16,6 @@ pub use graph_failure::*; pub use internal::*; pub type GraphResult<T> = Result<T, GraphFailure>; -pub type AuthorizationResult<T> = Result<T, AuthorizationFailure>; +pub type IdentityResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml index 6d20333b..28a9c3cb 100644 --- a/graph-extensions/Cargo.toml +++ b/graph-extensions/Cargo.toml @@ -10,6 +10,7 @@ description = "Extensions and utilities used across multiple crates that make up anyhow = { version = "1.0.69", features = ["backtrace"]} async-stream = "0.3" async-trait = "0.1.35" +base64 = "0.21.0" bytes = { version = "1.4.0", features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] } chrono-humanize = "0.2.2" @@ -19,6 +20,7 @@ http = "0.2.9" log = "0.4" pretty_env_logger = "0.4" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +ring = "0.16.20" serde-aux = "4.1.2" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -26,7 +28,7 @@ serde_urlencoded = "0.7.1" time = { version = "0.3.10", features = ["local-offset", "serde"] } tokio = { version = "1.27.0", features = ["full"] } url = { version = "2", features = ["serde"] } -wry = "0.30.0" +wry = "0.33.0" graph-error = { path = "../graph-error" } diff --git a/graph-extensions/src/cache/mod.rs b/graph-extensions/src/cache/mod.rs index f3d21c4d..9fa05f58 100644 --- a/graph-extensions/src/cache/mod.rs +++ b/graph-extensions/src/cache/mod.rs @@ -1,11 +1,13 @@ mod in_memory_credential_store; mod token_store; mod token_store_providers; +mod token_watch_task; pub use in_memory_credential_store::*; use std::fmt::{Debug, Formatter}; pub use token_store::*; pub use token_store_providers::*; +pub use token_watch_task::*; #[derive(Clone)] pub struct UnInitializedTokenStore; @@ -23,7 +25,11 @@ impl TokenStore for UnInitializedTokenStore { panic!("UnInitializedTokenStore does not store tokens") } - fn update_stored_token(&mut self, _id: &str, stored_token: StoredToken) -> Option<StoredToken> { + fn update_stored_token( + &mut self, + _id: &str, + _stored_token: StoredToken, + ) -> Option<StoredToken> { panic!("UnInitializedTokenStore does not store tokens") } diff --git a/graph-extensions/src/cache/token_watch_task.rs b/graph-extensions/src/cache/token_watch_task.rs new file mode 100644 index 00000000..e72795d2 --- /dev/null +++ b/graph-extensions/src/cache/token_watch_task.rs @@ -0,0 +1,21 @@ +use std::fmt::Debug; +pub use tokio::sync::watch::{channel, Receiver, Sender}; + +#[derive(Clone)] +pub struct AutomaticTokenRefresh<T> { + rx: Receiver<T>, +} + +impl<T: Clone + Debug + Send + Sync> AutomaticTokenRefresh<T> { + pub fn new(init: T) -> (Sender<T>, AutomaticTokenRefresh<T>) { + let (tx, mut rx) = channel(init); + + (tx, AutomaticTokenRefresh { rx }) + } + + pub async fn call(&mut self) { + while self.rx.changed().await.is_ok() { + println!("received = {:?}", *self.rx.borrow()); + } + } +} diff --git a/graph-extensions/src/crypto/mod.rs b/graph-extensions/src/crypto/mod.rs new file mode 100644 index 00000000..5befae25 --- /dev/null +++ b/graph-extensions/src/crypto/mod.rs @@ -0,0 +1,18 @@ +mod pkce; + +pub use pkce::*; + +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use graph_error::{IdentityResult, AF}; +use ring::rand::SecureRandom; + +pub fn secure_random_32() -> IdentityResult<String> { + let mut buf = [0; 32]; + + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| AF::unknown("ring::error::Unspecified"))?; + + Ok(URL_SAFE_NO_PAD.encode(buf)) +} diff --git a/graph-extensions/src/crypto/pkce.rs b/graph-extensions/src/crypto/pkce.rs new file mode 100644 index 00000000..710b471f --- /dev/null +++ b/graph-extensions/src/crypto/pkce.rs @@ -0,0 +1,143 @@ +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use ring::rand::SecureRandom; + +/* +pub(crate) fn sha256_secure_string() -> IdentityResult<(String, String)> { + let mut buf = [0; 32]; + + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| AuthorizationFailure::unknown("ring::error::Unspecified"))?; + + // Known as code_verifier in proof key for code exchange + let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); + + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(base_64_random_string.as_bytes()); + + // Known as code_challenge in proof key for code exchange + let secure_string = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + + // code verifier, code challenge + Ok((base_64_random_string, secure_string)) +} + */ + +pub trait GenPkce { + fn code_challenge_method() -> String { + "S256".into() + } + + /// Known as code_verifier in proof key for code exchange + /// Uses the Rust ring crypto library to generate a secure random + /// 32-octet sequence that is base64 URL encoded (no padding) + fn code_verifier() -> IdentityResult<String> { + let mut buf = [0; 32]; + + let rng = ring::rand::SystemRandom::new(); + rng.fill(&mut buf) + .map_err(|_| AuthorizationFailure::unknown("ring::error::Unspecified"))?; + + Ok(URL_SAFE_NO_PAD.encode(buf)) + } + + fn code_challenge(code_verifier: &String) -> IdentityResult<String> { + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(code_verifier.as_bytes()); + + // Known as code_challenge in proof key for code exchange + let code_challenge = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + + // code verifier, code challenge + Ok(code_challenge) + } + + /// Generate a code challenge and code verifier for the + /// authorization code grant flow using proof key for + /// code exchange (PKCE) and SHA256. + /// + /// [ProofKeyCodeExchange] contains a code_verifier, + /// code_challenge, and code_challenge_method for use in the authorization code grant. + /// + /// For authorization, the code_challenge_method parameter in the request body + /// is automatically set to 'S256'. + /// + /// Internally this method uses the Rust ring cyrpto library to generate a secure random + /// 32-octet sequence that is base64 URL encoded (no padding) and known as the code verifier. + /// This sequence is hashed using SHA256 and base64 URL encoded (no padding) resulting in a + /// 43-octet URL safe string which is known as the code challenge. + fn oneshot() -> IdentityResult<ProofKeyCodeExchange> { + let code_verifier = ProofKeyCodeExchange::code_verifier()?; + let code_challenge = ProofKeyCodeExchange::code_challenge(&code_verifier)?; + ProofKeyCodeExchange::new( + code_verifier, + code_challenge, + ProofKeyCodeExchange::code_challenge_method(), + ) + } +} + +impl GenPkce for ProofKeyCodeExchange {} + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct ProofKeyCodeExchange { + /// Used to verify the the + /// The code verifier is not included in the authorization URL. + /// You can set the code verifier here and then use the From trait + /// for [AuthorizationCodeCredential] which does use the code verifier. + pub code_verifier: String, + /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). + /// Required if code_challenge_method is included. For more information, see the PKCE RFC. + /// This parameter is now recommended for all application types, both public and confidential + /// clients, and required by the Microsoft identity platform for single page apps using the + /// authorization code flow. + pub code_challenge: String, + /// The method used to encode the code_verifier for the code_challenge parameter. + /// This SHOULD be S256, but the spec allows the use of plain if the client can't support SHA256. + /// + /// If excluded, code_challenge is assumed to be plaintext if code_challenge is included. + /// The Microsoft identity platform supports both plain and S256. + /// For more information, see the PKCE RFC. This parameter is required for single page + /// apps using the authorization code flow. + pub code_challenge_method: String, +} + +impl ProofKeyCodeExchange { + pub fn new<T: AsRef<str>>( + code_verifier: T, + code_challenge: T, + code_challenge_method: T, + ) -> IdentityResult<ProofKeyCodeExchange> { + let code_challenge = code_challenge.as_ref().to_owned(); + if code_challenge.len() != 43 { + return Err(AF::msg_err("code_challenge", "Must be 43-octet sequence")); + } + Ok(ProofKeyCodeExchange { + code_verifier: code_verifier.as_ref().to_owned(), + code_challenge, + code_challenge_method: code_challenge_method.as_ref().to_owned(), + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn pkce_generate() { + let pkce = ProofKeyCodeExchange::oneshot().unwrap(); + assert_eq!(pkce.code_challenge.len(), 43); + } + + #[test] + fn validate_pkce_challenge_and_verifier() { + let pkce = ProofKeyCodeExchange::oneshot().unwrap(); + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(pkce.code_verifier.as_bytes()); + let verifier = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); + assert_eq!(verifier, pkce.code_challenge); + } +} diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs index 373bd7dd..95decab1 100644 --- a/graph-extensions/src/lib.rs +++ b/graph-extensions/src/lib.rs @@ -5,6 +5,7 @@ extern crate log; extern crate pretty_env_logger; pub mod cache; +pub mod crypto; pub mod http; pub mod token; pub mod web; diff --git a/graph-extensions/src/token/client_application.rs b/graph-extensions/src/token/client_application.rs index 3980b94c..185e0632 100644 --- a/graph-extensions/src/token/client_application.rs +++ b/graph-extensions/src/token/client_application.rs @@ -13,8 +13,6 @@ dyn_clone::clone_trait_object!(ClientApplication); #[async_trait] pub trait ClientApplication: TokenStore + DynClone { - fn client_application_type(&self) -> ClientApplicationType; - fn get_token_silent(&mut self) -> AuthExecutionResult<String>; async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; diff --git a/graph-extensions/src/token/msal_token.rs b/graph-extensions/src/token/msal_token.rs index 02ce2ae1..f1a333b7 100644 --- a/graph-extensions/src/token/msal_token.rs +++ b/graph-extensions/src/token/msal_token.rs @@ -1,12 +1,10 @@ -use chrono::{DateTime, Duration, Utc}; -use chrono_humanize::HumanTime; use graph_error::GraphFailure; use serde::{Deserialize, Deserializer}; use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; -use std::ops::{Add, AddAssign}; +use std::ops::Add; use crate::token::IdToken; use std::str::FromStr; @@ -106,7 +104,7 @@ impl MsalToken { access_token: &str, scope: I, ) -> MsalToken { - let mut timestamp = time::OffsetDateTime::now_utc(); + let timestamp = time::OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(expires_in)); MsalToken { @@ -299,7 +297,7 @@ impl MsalToken { /// // The timestamp is in UTC. /// ``` pub fn gen_timestamp(&mut self) { - let mut timestamp = time::OffsetDateTime::now_utc(); + let timestamp = time::OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(self.expires_in.clone())); self.timestamp = Some(timestamp); self.expires_on = Some(expires_on); @@ -418,11 +416,6 @@ impl fmt::Debug for MsalToken { .field("state", &self.state) .field("timestamp", &self.timestamp) .field("expires_on", &self.expires_on) - .field( - "expires_result", - &time::OffsetDateTime::now_utc() - .checked_add(time::Duration::seconds(self.expires_in.clone())), - ) .field("additional_fields", &self.additional_fields) .finish() } else { @@ -446,11 +439,6 @@ impl fmt::Debug for MsalToken { .field("state", &self.state) .field("timestamp", &self.timestamp) .field("expires_on", &self.expires_on) - .field( - "expires_result", - &time::OffsetDateTime::now_utc() - .checked_add(time::Duration::seconds(self.expires_in.clone())), - ) .field("additional_fields", &self.additional_fields) .finish() } @@ -470,7 +458,7 @@ impl<'de> Deserialize<'de> for MsalToken { { let phantom_access_token: PhantomMsalToken = Deserialize::deserialize(deserializer)?; - let mut timestamp = time::OffsetDateTime::now_utc(); + let timestamp = time::OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); Ok(MsalToken { @@ -505,7 +493,7 @@ mod test { assert!(access_token.is_expired()); let mut access_token = MsalToken::default(); - access_token.with_expires_in(10); + access_token.with_expires_in(8); std::thread::sleep(std::time::Duration::from_secs(4)); assert!(!access_token.is_expired()); } diff --git a/graph-extensions/src/web/interactive_web_view.rs b/graph-extensions/src/web/interactive_web_view.rs index 7102c07a..b769ad11 100644 --- a/graph-extensions/src/web/interactive_web_view.rs +++ b/graph-extensions/src/web/interactive_web_view.rs @@ -3,7 +3,8 @@ use std::time::Duration; use url::Url; use crate::web::WebViewOptions; -use wry::application::platform::windows::EventLoopExtWindows; +use wry::application::event_loop::EventLoopBuilder; +use wry::application::platform::windows::EventLoopBuilderExtWindows; use wry::{ application::{ event::{Event, StartCause, WindowEvent}, @@ -66,7 +67,9 @@ impl InteractiveWebView { options: WebViewOptions, sender: std::sync::mpsc::Sender<String>, ) -> anyhow::Result<()> { - let event_loop: EventLoop<UserEvents> = EventLoop::<UserEvents>::new_any_thread(); + let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build(); let proxy = event_loop.create_proxy(); let sender2 = sender.clone(); diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index cc509b9a..a84bf2a2 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,13 +1,9 @@ use crate::blocking::BlockingClient; use async_trait::async_trait; use graph_error::AuthExecutionResult; -use graph_extensions::cache::{ - InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, -}; -use graph_extensions::token::{ClientApplication, ClientApplicationType}; -use graph_oauth::oauth::{ - ConfidentialClientApplication, PublicClientApplication, UnInitializedCredentialExecutor, -}; +use graph_extensions::cache::{StoredToken, TokenStore, TokenStoreProvider}; +use graph_extensions::token::ClientApplication; +use graph_oauth::oauth::{ConfidentialClientApplication, PublicClientApplication}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -29,33 +25,33 @@ impl TokenStore for BearerToken { TokenStoreProvider::InMemory } - fn is_stored_token_initialized(&self, id: &str) -> bool { + fn is_stored_token_initialized(&self, _id: &str) -> bool { true } - fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { + fn get_stored_token(&self, _id: &str) -> Option<&StoredToken> { None } - fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { + fn update_stored_token( + &mut self, + _id: &str, + _stored_token: StoredToken, + ) -> Option<StoredToken> { None } - fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { + fn get_bearer_token_from_store(&self, _id: &str) -> Option<&String> { Some(&self.0) } - fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { + fn get_refresh_token_from_store(&self, _id: &str) -> Option<&String> { None } } #[async_trait] impl ClientApplication for BearerToken { - fn client_application_type(&self) -> ClientApplicationType { - ClientApplicationType::PublicClientApplication - } - fn get_token_silent(&mut self) -> AuthExecutionResult<String> { Ok(self.0.clone()) } @@ -343,9 +339,21 @@ impl Debug for Client { } } +impl From<BearerToken> for Client { + fn from(value: BearerToken) -> Self { + Client::new(value) + } +} + +impl From<PublicClientApplication> for Client { + fn from(value: PublicClientApplication) -> Self { + Client::new(value) + } +} + impl From<ConfidentialClientApplication> for Client { fn from(value: ConfidentialClientApplication) -> Self { - todo!() + Client::new(value) } } @@ -375,17 +383,19 @@ mod test { assert_eq!("user_agent", user_agent_header.to_str().unwrap()); } - /* - #[test] + #[test] + #[should_panic] fn initialize_confidential_client() { - let client = GraphClientConfiguration::new() + let mut client = GraphClientConfiguration::new() .access_token("access_token") .user_agent(HeaderValue::from_static("user_agent")) - .build_with_client_application(ConfidentialClientApplication::builder("client-id") - .with_client_secret("secret") - .build()); + .client_application( + ConfidentialClientApplication::builder("client-id") + .with_client_secret("secret") + .build(), + ) + .build(); - assert!(client.client_application.get_stored_token()); + assert!(client.client_application.get_stored_token("").is_none()); } - */ } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 5c61ea3a..81946c41 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -4,7 +4,7 @@ use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; use crate::strum::IntoEnumIterator; use base64::Engine; -use graph_error::{AuthorizationFailure, AuthorizationResult, GraphFailure, GraphResult, AF}; +use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; use graph_extensions::token::{IdToken, MsalToken}; use ring::rand::SecureRandom; use std::collections::btree_map::{BTreeMap, Entry}; @@ -103,7 +103,6 @@ impl OAuthParameter { | OAuthParameter::CodeVerifier | OAuthParameter::CodeChallenge | OAuthParameter::Password - | OAuthParameter::AuthorizationCode ) } } @@ -980,11 +979,11 @@ impl OAuthSerializer { self.get(c).ok_or_else(|| OAuthError::credential_error(c)) } - pub fn ok_or(&self, oac: &OAuthParameter) -> AuthorizationResult<String> { + pub fn ok_or(&self, oac: &OAuthParameter) -> IdentityResult<String> { self.get(*oac).ok_or(AuthorizationFailure::required(oac)) } - pub fn try_as_tuple(&self, oac: &OAuthParameter) -> AuthorizationResult<(String, String)> { + pub fn try_as_tuple(&self, oac: &OAuthParameter) -> IdentityResult<(String, String)> { if oac.eq(&OAuthParameter::Scope) { if self.scopes.is_empty() { return Err(AuthorizationFailure::required(oac)); @@ -1020,7 +1019,7 @@ impl OAuthSerializer { optional_fields: Vec<OAuthParameter>, required_fields: Vec<OAuthParameter>, encoder: &mut Serializer<String>, - ) -> AuthorizationResult<()> { + ) -> IdentityResult<()> { for parameter in required_fields { if parameter.alias().eq("scope") { if self.scopes.is_empty() { @@ -1070,11 +1069,11 @@ impl OAuthSerializer { &mut self, optional_fields: Vec<OAuthParameter>, required_fields: Vec<OAuthParameter>, - ) -> AuthorizationResult<HashMap<String, String>> { + ) -> IdentityResult<HashMap<String, String>> { let mut required_map = required_fields .iter() .map(|oac| self.try_as_tuple(oac)) - .collect::<AuthorizationResult<HashMap<String, String>>>()?; + .collect::<IdentityResult<HashMap<String, String>>>()?; let optional_map: HashMap<String, String> = optional_fields .iter() diff --git a/graph-oauth/src/discovery/graph_discovery.rs b/graph-oauth/src/discovery/graph_discovery.rs index 1fc7bba2..e39f86a3 100644 --- a/graph-oauth/src/discovery/graph_discovery.rs +++ b/graph-oauth/src/discovery/graph_discovery.rs @@ -1,6 +1,3 @@ -use crate::oauth::well_known::WellKnown; -use crate::oauth::{OAuthError, OAuthSerializer}; - static LOGIN_LIVE_HOST: &str = "https://login.live.com"; static MICROSOFT_ONLINE_HOST: &str = "https://login.microsoftonline.com"; static OPEN_ID_PATH: &str = ".well-known/openid-configuration"; @@ -52,131 +49,29 @@ pub struct MicrosoftSigningKeysV2 { pub rbac_url: String, } -pub enum GraphDiscovery { +pub enum SigningKeys { V1, V2, Tenant(String), } -impl GraphDiscovery { +impl SigningKeys { /// Get the URL for the public keys used by the Microsoft identity platform /// to sign security tokens. /// /// # Example /// ``` - /// # use graph_oauth::oauth::graph_discovery::GraphDiscovery; - /// let url = GraphDiscovery::V1.url(); + /// # use graph_oauth::oauth::graph_discovery::SigningKeys; + /// let url = SigningKeys::V1.url(); /// println!("{}", url); /// ``` pub fn url(&self) -> String { match self { - GraphDiscovery::V1 => format!("{LOGIN_LIVE_HOST}/{OPEN_ID_PATH}"), - GraphDiscovery::V2 => format!("{MICROSOFT_ONLINE_HOST}/common/v2.0/{OPEN_ID_PATH}"), - GraphDiscovery::Tenant(tenant) => { + SigningKeys::V1 => format!("{LOGIN_LIVE_HOST}/{OPEN_ID_PATH}"), + SigningKeys::V2 => format!("{MICROSOFT_ONLINE_HOST}/common/v2.0/{OPEN_ID_PATH}"), + SigningKeys::Tenant(tenant) => { format!("{MICROSOFT_ONLINE_HOST}/{tenant}/v2.0/{OPEN_ID_PATH}") } } } - - /// Get the public keys used by the Microsoft identity platform - /// to sign security tokens. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::graph_discovery::GraphDiscovery; - /// let keys: serde_json::Value = GraphDiscovery::V1.signing_keys().unwrap(); - /// println!("{:#?}", keys); - /// ``` - pub fn signing_keys<T>(self) -> Result<T, OAuthError> - where - for<'de> T: serde::Deserialize<'de>, - { - let t: T = WellKnown::signing_keys(self.url().as_str())?; - Ok(t) - } - - /// Get the public keys used by the Microsoft identity platform - /// to sign security tokens. - /// - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::graphdiscovery::GraphDiscovery; - /// let keys: serde_json::Value = GraphDiscovery::V1.async_signing_keys().await.unwrap(); - /// println!("{:#?}", keys); - /// ``` - pub async fn async_signing_keys<T>(self) -> Result<T, OAuthError> - where - for<'de> T: serde::Deserialize<'de>, - { - let t: T = WellKnown::async_signing_keys(self.url().as_str()).await?; - Ok(t) - } - - /// Automatically convert the public keys used by the Microsoft identity platform - /// to sign security tokens into an OAuth object. This will get the common urls - /// for authorization and access tokens and insert them into OAuth. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::graph_discovery::GraphDiscovery; - /// let oauth = GraphDiscovery::V1.oauth().unwrap(); - /// println!("{:#?}", oauth); - /// ``` - pub fn oauth(self) -> Result<OAuthSerializer, OAuthError> { - let mut oauth = OAuthSerializer::new(); - match self { - GraphDiscovery::V1 => { - let k: MicrosoftSigningKeysV1 = self.signing_keys()?; - oauth - .authorization_url(k.authorization_endpoint.as_str()) - .token_uri(k.token_endpoint.as_str()) - .refresh_token_url(k.token_endpoint.as_str()) - .logout_url(k.end_session_endpoint.as_str()); - Ok(oauth) - } - GraphDiscovery::V2 | GraphDiscovery::Tenant(_) => { - let k: MicrosoftSigningKeysV2 = self.signing_keys()?; - oauth - .authorization_url(k.authorization_endpoint.as_str()) - .token_uri(k.token_endpoint.as_str()) - .refresh_token_url(k.token_endpoint.as_str()) - .logout_url(k.end_session_endpoint.as_str()); - Ok(oauth) - } - } - } - - /// Automatically convert the public keys used by the Microsoft identity platform - /// to sign security tokens into an OAuth object. This will get the common urls - /// for authorization and access tokens and insert them into OAuth. - /// - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::graphdiscovery::GraphDiscovery; - /// let oauth = GraphDiscovery::V1.async_oauth().await.unwrap(); - /// println!("{:#?}", oauth); - /// ``` - pub async fn async_oauth(self) -> Result<OAuthSerializer, OAuthError> { - let mut oauth = OAuthSerializer::new(); - match self { - GraphDiscovery::V1 => { - let k: MicrosoftSigningKeysV1 = self.async_signing_keys().await?; - oauth - .authorization_url(k.authorization_endpoint.as_str()) - .token_uri(k.token_endpoint.as_str()) - .refresh_token_url(k.token_endpoint.as_str()) - .logout_url(k.end_session_endpoint.as_str()); - Ok(oauth) - } - GraphDiscovery::V2 | GraphDiscovery::Tenant(_) => { - let k: MicrosoftSigningKeysV2 = self.async_signing_keys().await?; - oauth - .authorization_url(k.authorization_endpoint.as_str()) - .token_uri(k.token_endpoint.as_str()) - .refresh_token_url(k.token_endpoint.as_str()) - .logout_url(k.end_session_endpoint.as_str()); - Ok(oauth) - } - } - } } diff --git a/graph-oauth/src/discovery/mod.rs b/graph-oauth/src/discovery/mod.rs index a3afcf51..8271586d 100644 --- a/graph-oauth/src/discovery/mod.rs +++ b/graph-oauth/src/discovery/mod.rs @@ -1,3 +1,2 @@ pub mod graph_discovery; pub mod jwt_keys; -pub mod well_known; diff --git a/graph-oauth/src/discovery/well_known.rs b/graph-oauth/src/discovery/well_known.rs deleted file mode 100644 index 2f717274..00000000 --- a/graph-oauth/src/discovery/well_known.rs +++ /dev/null @@ -1,26 +0,0 @@ -use graph_error::GraphResult; - -#[derive(Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct WellKnown; - -impl WellKnown { - pub fn signing_keys<T>(url: &str) -> GraphResult<T> - where - for<'de> T: serde::Deserialize<'de>, - { - let client = reqwest::blocking::Client::new(); - let response = client.get(url).send()?; - let keys: T = response.json()?; - Ok(keys) - } - - pub async fn async_signing_keys<T>(url: &str) -> GraphResult<T> - where - for<'de> T: serde::Deserialize<'de>, - { - let client = reqwest::Client::new(); - let response = client.get(url).send().await?; - let keys: T = response.json().await?; - Ok(keys) - } -} diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index 8e8df32f..1493477b 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -1,11 +1,11 @@ use crate::identity::AzureCloudInstance; -use graph_error::AuthorizationResult; +use graph_error::IdentityResult; use std::collections::HashMap; use url::Url; pub trait AuthorizationSerializer { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url>; - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url>; + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; fn basic_auth(&self) -> Option<(String, String)> { None } @@ -13,9 +13,9 @@ pub trait AuthorizationSerializer { pub trait AuthorizationUrl { fn redirect_uri(&self) -> Option<&Url>; - fn authorization_url(&self) -> AuthorizationResult<Url>; + fn authorization_url(&self) -> IdentityResult<Url>; fn authorization_url_with_host( &self, azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url>; + ) -> IdentityResult<Url>; } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 5517aacc..94ff6875 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -28,7 +28,7 @@ pub struct AppConfig { } impl AppConfig { - pub fn new() -> AppConfig { + pub(crate) fn new() -> AppConfig { AppConfig { tenant_id: None, client_id: Uuid::default(), diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index fe4f6846..7a02836c 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,7 +1,7 @@ -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::credentials::client_assertion_credential::ClientAssertionCredentialBuilder; use crate::identity::{ - application_options::ApplicationOptions, AuthCodeAuthorizationUrlParameterBuilder, Authority, + application_options::ApplicationOptions, credentials::app_config::AppConfig, + credentials::client_assertion_credential::ClientAssertionCredentialBuilder, + AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, @@ -10,7 +10,8 @@ use crate::identity::{ }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; -use graph_error::{AuthorizationResult, AF}; +use crate::oauth::OpenIdAuthorizationUrlBuilder; +use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::env::VarError; @@ -56,7 +57,7 @@ impl ConfidentialClientApplicationBuilder { pub fn new_with_application_options( application_options: ApplicationOptions, - ) -> AuthorizationResult<ConfidentialClientApplicationBuilder> { + ) -> IdentityResult<ConfidentialClientApplicationBuilder> { ConfidentialClientApplicationBuilder::try_from(application_options) } @@ -110,7 +111,9 @@ impl ConfidentialClientApplicationBuilder { self } - pub fn authorization_code_url_builder(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { + pub fn auth_code_authorization_url_builder( + &mut self, + ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) } @@ -120,8 +123,8 @@ impl ConfidentialClientApplicationBuilder { ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) } - pub fn openid_authorization_url_builder(&mut self) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) + pub fn openid_authorization_url_builder(&mut self) -> OpenIdAuthorizationUrlBuilder { + OpenIdAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) } #[cfg(feature = "openssl")] @@ -253,7 +256,7 @@ impl PublicClientApplicationBuilder { #[allow(dead_code)] pub fn create_with_application_options( application_options: ApplicationOptions, - ) -> AuthorizationResult<PublicClientApplicationBuilder> { + ) -> IdentityResult<PublicClientApplicationBuilder> { PublicClientApplicationBuilder::try_from(application_options) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs similarity index 93% rename from graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs rename to graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index f3159596..ef852cf3 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url_parameters.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,14 +1,15 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, - ResponseMode, + Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Prompt, + ResponseMode, ResponseType, }; -use crate::oauth::{ProofKeyForCodeExchange, ResponseType}; + use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; +use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; use reqwest::IntoUrl; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; @@ -28,6 +29,25 @@ use uuid::Uuid; /// by a user to sign in to your app and access their data. /// /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code +/// +/// # Build a confidential client for the authorization code grant. +/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_authorization_code) to set the authorization code received from +/// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) +/// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder) +/// to build the url that the user will be directed to authorize at. +/// +/// ```rust +/// fn main() { +/// # use graph_oauth::identity::ConfidentialClientApplication; +/// +/// // +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("access-code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); +/// } +/// ``` #[derive(Clone, Debug)] pub struct AuthCodeAuthorizationUrlParameters { pub(crate) app_config: AppConfig, @@ -59,7 +79,7 @@ impl AuthCodeAuthorizationUrlParameters { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, redirect_uri: U, - ) -> AuthorizationResult<AuthCodeAuthorizationUrlParameters> { + ) -> IdentityResult<AuthCodeAuthorizationUrlParameters> { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); let redirect_uri_result = Url::parse(redirect_uri.as_str()); @@ -91,14 +111,11 @@ impl AuthCodeAuthorizationUrlParameters { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.url_with_host(&AzureCloudInstance::default()) } - pub fn url_with_host( - &self, - azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { self.authorization_url_with_host(azure_cloud_instance) } @@ -197,14 +214,14 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { self.app_config.redirect_uri.as_ref() } - fn authorization_url(&self) -> AuthorizationResult<Url> { + fn authorization_url(&self) -> IdentityResult<Url> { self.authorization_url_with_host(&AzureCloudInstance::default()) } fn authorization_url_with_host( &self, azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + ) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { @@ -442,8 +459,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] - pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.parameters.nonce = Some(Crypto::sha256_secure_string()?.1); + pub(crate) fn with_nonce_generated(&mut self) -> IdentityResult<&mut Self> { + self.parameters.nonce = Some(secure_random_32()?); Ok(self) } @@ -554,13 +571,10 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - /// Sets the code_challenge and code_challenge_method using the [ProofKeyForCodeExchange] - /// Callers should keep the [ProofKeyForCodeExchange] and provide it to the credential + /// Sets the code_challenge and code_challenge_method using the [ProofKeyCodeExchange] + /// Callers should keep the [ProofKeyCodeExchange] and provide it to the credential /// builder in order to set the client verifier and request an access token. - pub fn with_pkce( - &mut self, - proof_key_for_code_exchange: &ProofKeyForCodeExchange, - ) -> &mut Self { + pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self { self.with_code_challenge(proof_key_for_code_exchange.code_challenge.as_str()); self.with_code_challenge_method(proof_key_for_code_exchange.code_challenge_method.as_str()); self @@ -570,7 +584,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self.parameters.clone() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.parameters.url() } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index abc8334d..463f3b8f 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -6,7 +6,7 @@ use crate::identity::{ CLIENT_ASSERTION_TYPE, }; use async_trait::async_trait; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -61,7 +61,7 @@ impl AuthorizationCodeCertificateCredential { authorization_code: T, client_assertion: T, redirect_uri: Option<U>, - ) -> AuthorizationResult<AuthorizationCodeCertificateCredential> { + ) -> IdentityResult<AuthorizationCodeCertificateCredential> { let redirect_uri = { if let Some(redirect_uri) = redirect_uri { redirect_uri.into_url().ok() @@ -110,7 +110,7 @@ impl AuthorizationCodeCertificateCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.authority()); @@ -122,7 +122,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { Url::parse(uri.as_str()).map_err(AF::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId); diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 86b24d4d..7d9368ea 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,12 +1,12 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ProofKeyForCodeExchange, - TokenCredentialExecutor, + Authority, AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use async_trait::async_trait; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; +use graph_extensions::crypto::ProofKeyCodeExchange; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -64,7 +64,7 @@ impl AuthorizationCodeCredential { client_id: T, client_secret: T, authorization_code: T, - ) -> AuthorizationResult<AuthorizationCodeCredential> { + ) -> IdentityResult<AuthorizationCodeCredential> { Ok(AuthorizationCodeCredential { app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), authorization_code: Some(authorization_code.as_ref().to_owned()), @@ -82,7 +82,7 @@ impl AuthorizationCodeCredential { client_secret: T, authorization_code: T, redirect_uri: U, - ) -> AuthorizationResult<AuthorizationCodeCredential> { + ) -> IdentityResult<AuthorizationCodeCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; @@ -194,10 +194,7 @@ impl AuthorizationCodeCredentialBuilder { self } - pub fn with_pkce( - &mut self, - proof_key_for_code_exchange: &ProofKeyForCodeExchange, - ) -> &mut Self { + pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self { self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); self } @@ -211,7 +208,7 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.authority()); @@ -223,7 +220,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { Url::parse(uri.as_str()).map_err(AF::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 92d74716..97e7e049 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -4,7 +4,7 @@ use crate::identity::{ }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; use async_trait::async_trait; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; @@ -94,7 +94,7 @@ impl ClientAssertionCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.authority()); @@ -106,7 +106,7 @@ impl TokenCredentialExecutor for ClientAssertionCredential { Url::parse(uri.as_str()).map_err(AF::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.client_id().to_string(); if client_id.trim().is_empty() { return AF::result(OAuthParameter::ClientId.alias()); diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 76b688ac..79996bdc 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; @@ -76,7 +76,7 @@ impl ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.authority()); @@ -88,7 +88,7 @@ impl TokenCredentialExecutor for ClientCertificateCredential { Url::parse(uri.as_str()).map_err(AF::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 68802e5c..1e03c1e1 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,7 +1,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance}; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, IdentityResult}; use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; @@ -18,7 +18,7 @@ impl ClientCredentialsAuthorizationUrl { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, redirect_uri: U, - ) -> AuthorizationResult<ClientCredentialsAuthorizationUrl> { + ) -> IdentityResult<ClientCredentialsAuthorizationUrl> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; @@ -40,14 +40,11 @@ impl ClientCredentialsAuthorizationUrl { ClientCredentialsAuthorizationUrlBuilder::new(client_id) } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.url_with_host(&self.app_config.azure_cloud_instance) } - pub fn url_with_host( - &self, - azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.trim().is_empty() || self.app_config.client_id.is_nil() { @@ -119,18 +116,12 @@ impl ClientCredentialsAuthorizationUrlBuilder { } } - pub fn with_client_id<T: AsRef<str>>( - &mut self, - client_id: T, - ) -> AuthorizationResult<&mut Self> { + pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> IdentityResult<&mut Self> { self.parameters.app_config.client_id = Uuid::try_parse(client_id.as_ref())?; Ok(self) } - pub fn with_redirect_uri<T: IntoUrl>( - &mut self, - redirect_uri: T, - ) -> AuthorizationResult<&mut Self> { + pub fn with_redirect_uri<T: IntoUrl>(&mut self, redirect_uri: T) -> IdentityResult<&mut Self> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; self.parameters.app_config.redirect_uri = Some(redirect_uri); @@ -157,7 +148,7 @@ impl ClientCredentialsAuthorizationUrlBuilder { self.parameters.clone() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.parameters.url() } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index a3863f7b..50a6cf52 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -6,7 +6,7 @@ use crate::identity::{ }; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, IdentityResult}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use url::Url; @@ -77,7 +77,7 @@ impl ClientSecretCredential { #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.authority()); @@ -92,7 +92,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId); diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 8445b294..c281dbfb 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -8,30 +8,50 @@ use crate::identity::{ }; use async_trait::async_trait; -use graph_error::{AuthExecutionResult, AuthorizationResult, AF}; +use graph_error::{AuthExecutionResult, IdentityResult, AF}; use crate::oauth::MsalToken; use graph_extensions::cache::{ - InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, UnInitializedTokenStore, + AutomaticTokenRefresh, InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, + UnInitializedTokenStore, }; -use graph_extensions::token::{ClientApplication, ClientApplicationType}; -use http::header::ACCEPT; -use reqwest::header::{HeaderValue, CONTENT_TYPE}; +use graph_extensions::token::ClientApplication; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; use std::collections::HashMap; use url::Url; use uuid::Uuid; -use wry::http::HeaderMap; /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. +/// +/// +/// # Build a confidential client for the authorization code grant. +/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_authorization_code) to set the authorization code received from +/// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) +/// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder) +/// to build the url that the user will be directed to authorize at. +/// +/// ```rust +/// fn main() { +/// # use graph_oauth::identity::ConfidentialClientApplication; +/// +/// // +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("access-code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); +/// } +/// ``` #[derive(Clone)] pub struct ConfidentialClientApplication { http_client: reqwest::Client, credential: Box<dyn TokenCredentialExecutor + Send>, token_store: Box<dyn TokenStore + Send>, + token_watch: AutomaticTokenRefresh<String>, + token_sender: tokio::sync::watch::Sender<String>, } impl ConfidentialClientApplication { @@ -46,6 +66,8 @@ impl ConfidentialClientApplication { where T: TokenCredentialExecutor + Send + 'static, { + let (token_sender, token_watch) = AutomaticTokenRefresh::new(String::new()); + ConfidentialClientApplication { http_client: ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -54,6 +76,8 @@ impl ConfidentialClientApplication { .unwrap(), credential: Box::new(credential), token_store: Box::new(UnInitializedTokenStore), + token_watch, + token_sender, } } @@ -61,6 +85,15 @@ impl ConfidentialClientApplication { ConfidentialClientApplicationBuilder::new(client_id) } + pub fn init_automatic_refresh_token(&mut self) { + let rx = self.token_sender.subscribe(); + tokio::spawn(async move { + while rx.changed().await.is_ok() { + println!("received = {:?}", *rx.borrow()); + } + }); + } + pub fn with_in_memory_token_store(&mut self) { self.token_store = Box::new(InMemoryCredentialStore::new( self.app_config().cache_id(), @@ -68,7 +101,8 @@ impl ConfidentialClientApplication { )); } - fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { + /* + fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let response = self.get_openid_config()?; let config: serde_json::Value = response.json()?; let user_info_endpoint = Url::parse(config["userinfo_endpoint"].as_str().unwrap()).unwrap(); @@ -96,14 +130,11 @@ impl ConfidentialClientApplication { Ok(response) } + */ } #[async_trait] impl ClientApplication for ConfidentialClientApplication { - fn client_application_type(&self) -> ClientApplicationType { - ClientApplicationType::ConfidentialClientApplication - } - fn get_token_silent(&mut self) -> AuthExecutionResult<String> { let cache_id = self.app_config().cache_id(); if self.is_store_and_token_initialized(cache_id.as_str()) { @@ -194,11 +225,11 @@ impl TokenStore for ConfidentialClientApplication { #[async_trait] impl TokenCredentialExecutor for ConfidentialClientApplication { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { self.credential.form_urlencode() } diff --git a/graph-oauth/src/identity/credentials/crypto.rs b/graph-oauth/src/identity/credentials/crypto.rs deleted file mode 100644 index 7848911e..00000000 --- a/graph-oauth/src/identity/credentials/crypto.rs +++ /dev/null @@ -1,37 +0,0 @@ -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine; -use graph_error::{AuthorizationFailure, AuthorizationResult}; -use ring::rand::SecureRandom; - -pub struct Crypto; - -impl Crypto { - /// Generate a secure 43-octet URL safe string for use as a nonce - /// parameter or in the proof key for code exchange (PKCE) flow. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - /// - /// For more info on PKCE and entropy see: <https://tools.ietf.org/html/rfc7519#section-7.2> - pub fn sha256_secure_string() -> AuthorizationResult<(String, String)> { - let mut buf = [0; 32]; - - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| AuthorizationFailure::unknown("ring::error::Unspecified"))?; - - // Known as code_verifier in proof key for code exchange - let base_64_random_string = URL_SAFE_NO_PAD.encode(buf); - - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(base_64_random_string.as_bytes()); - - // Known as code_challenge in proof key for code exchange - let secure_string = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - - // code verifier, code challenge - Ok((base_64_random_string, secure_string)) - } -} diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4a2f4ab0..3b47dd45 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -4,7 +4,7 @@ use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, - AuthorizationResult, AF, + IdentityResult, AF, }; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; @@ -83,7 +83,7 @@ impl DeviceCodeCredential { } impl TokenCredentialExecutor for DeviceCodeCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority_device_code(&azure_cloud_instance, &self.authority()); @@ -103,7 +103,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { } } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 0b77e745..b2feb293 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -6,7 +6,7 @@ use crate::identity::{ use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, }; -use graph_error::AuthorizationResult; +use graph_error::IdentityResult; use std::collections::HashMap; use std::env::VarError; use url::Url; @@ -126,29 +126,27 @@ impl EnvironmentCredential { } } +/* impl AuthorizationSerializer for EnvironmentCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { self.credential.uri() } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { self.credential.form_urlencode() } } + */ impl TokenCredentialExecutor for EnvironmentCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { self.credential.form_urlencode() } - fn azure_cloud_instance(&self) -> AzureCloudInstance { - self.app_config().azure_cloud_instance - } - fn client_id(&self) -> &Uuid { self.credential.client_id() } @@ -157,6 +155,10 @@ impl TokenCredentialExecutor for EnvironmentCredential { self.credential.authority() } + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config().azure_cloud_instance + } + fn app_config(&self) -> &AppConfig { self.credential.app_config() } diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/implicit_credential.rs index 915f2b5d..0d98e6ac 100644 --- a/graph-oauth/src/identity/credentials/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/implicit_credential.rs @@ -1,8 +1,9 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{AzureCloudInstance, Crypto, Prompt, ResponseMode, ResponseType}; +use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, IdentityResult}; +use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -104,14 +105,11 @@ impl ImplicitCredential { ImplicitCredentialBuilder::new() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.url_with_host(&AzureCloudInstance::default()) } - pub fn url_with_host( - &self, - azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { @@ -298,8 +296,8 @@ impl ImplicitCredentialBuilder { /// generate a secure random 32-octet sequence that is base64 URL /// encoded (no padding). This sequence is hashed using SHA256 and /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.credential.nonce = Crypto::sha256_secure_string()?.1; + pub fn with_nonce_generated(&mut self) -> IdentityResult<&mut Self> { + self.credential.nonce = secure_random_32()?; Ok(self) } @@ -333,7 +331,7 @@ impl ImplicitCredentialBuilder { self } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.credential.url() } diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index 2352f331..46532740 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::oauth::ResponseType; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, IdentityResult}; use url::form_urlencoded::Serializer; use url::Url; @@ -46,7 +46,7 @@ impl CodeFlowAuthorizationUrl { CodeFlowAuthorizationUrlBuilder::new() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::result("redirect_uri"); diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs index 71126ba7..4aa4f251 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{Authority, AuthorizationSerializer, AzureCloudInstance}; -use graph_error::{AuthorizationFailure, AuthorizationResult}; +use graph_error::{AuthorizationFailure, IdentityResult}; use std::collections::HashMap; use url::Url; @@ -59,7 +59,7 @@ impl CodeFlowCredential { } impl AuthorizationSerializer for CodeFlowCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> AuthorizationResult<Url> { + fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { self.serializer .authority(azure_cloud_instance, &Authority::Common); @@ -76,7 +76,7 @@ impl AuthorizationSerializer for CodeFlowCredential { } } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { if self.client_id.trim().is_empty() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index 563bd164..4557ff5e 100644 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -1,6 +1,6 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::oauth::ResponseType; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; use url::form_urlencoded::Serializer; use url::Url; @@ -33,7 +33,7 @@ impl TokenFlowAuthorizationUrl { TokenFlowAuthorizationUrlBuilder::new() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); if self.redirect_uri.trim().is_empty() { return AuthorizationFailure::result("redirect_uri"); diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 20ffabe2..40d6dcf0 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -6,7 +6,7 @@ pub mod legacy; mod app_config; mod application_builder; mod as_query; -mod auth_code_authorization_url_parameters; +mod auth_code_authorization_url; mod authorization_code_certificate_credential; mod authorization_code_credential; mod client_assertion_credential; @@ -14,7 +14,6 @@ mod client_certificate_credential; mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; -mod crypto; mod device_code_credential; mod display; mod environment_credential; @@ -22,7 +21,6 @@ mod implicit_credential; mod open_id_authorization_url; mod open_id_credential; mod prompt; -mod proof_key_for_code_exchange; mod public_client_application; mod resource_owner_password_credential; mod response_mode; @@ -34,8 +32,9 @@ mod token_request; #[cfg(feature = "openssl")] mod x509_certificate; +pub use application_builder::*; pub use as_query::*; -pub use auth_code_authorization_url_parameters::*; +pub use auth_code_authorization_url::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use client_builder_impl::*; @@ -43,7 +42,6 @@ pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; pub use confidential_client_application::*; -pub(crate) use crypto::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; @@ -51,7 +49,6 @@ pub use implicit_credential::*; pub use open_id_authorization_url::*; pub use open_id_credential::*; pub use prompt::*; -pub use proof_key_for_code_exchange::*; pub use public_client_application::*; pub use resource_owner_password_credential::*; pub use response_mode::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index d3d86796..fba21488 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -1,10 +1,10 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Crypto, Prompt, ResponseMode, - ResponseType, + AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; -use graph_error::{AuthorizationFailure, AuthorizationResult, AF}; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; use reqwest::IntoUrl; use std::collections::BTreeSet; use url::form_urlencoded::Serializer; @@ -101,14 +101,14 @@ impl OpenIdAuthorizationUrl { client_id: T, redirect_uri: IU, scope: I, - ) -> AuthorizationResult<OpenIdAuthorizationUrl> { + ) -> IdentityResult<OpenIdAuthorizationUrl> { let mut scope_set = BTreeSet::new(); scope_set.insert("openid".to_owned()); scope_set.extend(scope.into_iter().map(|s| s.to_string())); let redirect_uri_result = Url::parse(redirect_uri.as_str()); - Ok(OpenIdAuthorizationUrl { - app_config: AppConfig { + /* + AppConfig { tenant_id: None, client_id: Uuid::try_parse(client_id.as_ref())?, authority: Default::default(), @@ -116,10 +116,17 @@ impl OpenIdAuthorizationUrl { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - }, + } + */ + + let mut app_config = AppConfig::new_with_client_id(client_id); + app_config.redirect_uri = Some(redirect_uri.into_url().or(redirect_uri_result)?); + + Ok(OpenIdAuthorizationUrl { + app_config, response_type: BTreeSet::new(), response_mode: None, - nonce: Crypto::sha256_secure_string()?.1, + nonce: secure_random_32()?, state: None, scope: scope_set, prompt: BTreeSet::new(), @@ -134,18 +141,15 @@ impl OpenIdAuthorizationUrl { }) } - pub fn builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { - OpenIdAuthorizationUrlBuilder::new() + pub fn builder(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { + OpenIdAuthorizationUrlBuilder::new(client_id) } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn url(&self) -> IdentityResult<Url> { self.url_with_host(&AzureCloudInstance::default()) } - pub fn url_with_host( - &self, - azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { self.authorization_url_with_host(azure_cloud_instance) } @@ -165,14 +169,14 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { self.app_config.redirect_uri.as_ref() } - fn authorization_url(&self) -> AuthorizationResult<Url> { + fn authorization_url(&self) -> IdentityResult<Url> { self.authorization_url_with_host(&AzureCloudInstance::default()) } fn authorization_url_with_host( &self, azure_cloud_instance: &AzureCloudInstance, - ) -> AuthorizationResult<Url> { + ) -> IdentityResult<Url> { let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); @@ -275,16 +279,24 @@ pub struct OpenIdAuthorizationUrlBuilder { } impl OpenIdAuthorizationUrlBuilder { - pub(crate) fn new() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { + pub(crate) fn new(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { let mut scope = BTreeSet::new(); scope.insert("openid".to_owned()); Ok(OpenIdAuthorizationUrlBuilder { auth_url_parameters: OpenIdAuthorizationUrl { - app_config: AppConfig::default(), + app_config: AppConfig { + tenant_id: None, + client_id: Uuid::try_parse(client_id.as_ref())?, + authority: Default::default(), + azure_cloud_instance: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + }, response_type: BTreeSet::new(), response_mode: None, - nonce: Crypto::sha256_secure_string()?.1, + nonce: secure_random_32()?, state: None, scope, prompt: Default::default(), @@ -300,10 +312,43 @@ impl OpenIdAuthorizationUrlBuilder { }) } + pub(crate) fn new_with_app_config(app_config: AppConfig) -> OpenIdAuthorizationUrlBuilder { + let mut scope = BTreeSet::new(); + scope.insert("openid".to_owned()); + + let nonce = match ProofKeyCodeExchange::code_verifier() { + Ok(secure_string) => secure_string, + Err(err) => { + error!("OpenIdAuthorizationUrlBuilder nonce: Crypto::sha256_secure_string() - internal error please report"); + panic!("{}", err); + } + }; + + OpenIdAuthorizationUrlBuilder { + auth_url_parameters: OpenIdAuthorizationUrl { + app_config, + response_type: BTreeSet::new(), + response_mode: None, + nonce, + state: None, + scope, + prompt: Default::default(), + domain_hint: None, + login_hint: None, + response_types_supported: vec![ + "code".into(), + "id_token".into(), + "code id_token".into(), + "id_token token".into(), + ], + }, + } + } + pub fn with_redirect_uri<T: AsRef<str>>( &mut self, redirect_uri: T, - ) -> anyhow::Result<&mut Self> { + ) -> IdentityResult<&mut Self> { self.auth_url_parameters.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); Ok(self) } @@ -389,7 +434,7 @@ impl OpenIdAuthorizationUrlBuilder { /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.nonce = Crypto::sha256_secure_string()?.1; + self.auth_url_parameters.nonce = secure_random_32()?; Ok(self) } @@ -400,20 +445,11 @@ impl OpenIdAuthorizationUrlBuilder { /// Takes an iterator of scopes to use in the request. /// Replaces current scopes if any were added previously. - /// To extend scopes use [OpenIdAuthorizationUrlBuilder::extend_scope]. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { self.auth_url_parameters.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } - /// Extend the current list of scopes. - pub fn extend_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.auth_url_parameters - .scope - .extend(scope.into_iter().map(|s| s.to_string())); - self - } - /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// @@ -453,7 +489,11 @@ impl OpenIdAuthorizationUrlBuilder { self.auth_url_parameters.clone() } - pub fn url(&self) -> AuthorizationResult<Url> { + pub fn nonce(&self) -> &String { + &self.auth_url_parameters.nonce + } + + pub fn url(&self) -> IdentityResult<Url> { self.auth_url_parameters.url() } } @@ -465,10 +505,9 @@ mod test { #[test] #[should_panic] fn unsupported_response_type() { - let _ = OpenIdAuthorizationUrl::builder() + let _ = OpenIdAuthorizationUrl::builder("client_id") .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) - .with_client_id("client_id") .with_scope(["scope"]) .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index c0868f5f..dd4cc15b 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,12 +1,12 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, OpenIdAuthorizationUrl, ProofKeyForCodeExchange, - TokenCredentialExecutor, + Authority, AzureCloudInstance, OpenIdAuthorizationUrl, TokenCredentialExecutor, }; use crate::oauth::{ConfidentialClientApplication, OpenIdAuthorizationUrlBuilder}; use async_trait::async_trait; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; +use graph_extensions::crypto::{GenPkce, ProofKeyCodeExchange}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -55,7 +55,7 @@ pub struct OpenIdCredential { pub(crate) code_verifier: Option<String>, /// Used only when the client generates the pkce itself when the generate method /// is called. - pub(crate) pkce: Option<ProofKeyForCodeExchange>, + pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: OAuthSerializer, } @@ -65,7 +65,7 @@ impl OpenIdCredential { client_secret: T, authorization_code: T, redirect_uri: U, - ) -> AuthorizationResult<OpenIdCredential> { + ) -> IdentityResult<OpenIdCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); Ok(OpenIdCredential { app_config: AppConfig { @@ -96,18 +96,18 @@ impl OpenIdCredential { OpenIdCredentialBuilder::new() } - pub fn authorization_url_builder() -> AuthorizationResult<OpenIdAuthorizationUrlBuilder> { - OpenIdAuthorizationUrlBuilder::new() + pub fn authorization_url_builder(client_id: impl AsRef<str>) -> OpenIdAuthorizationUrlBuilder { + OpenIdAuthorizationUrlBuilder::new_with_app_config(AppConfig::new_with_client_id(client_id)) } - pub fn pkce(&self) -> Option<&ProofKeyForCodeExchange> { + pub fn pkce(&self) -> Option<&ProofKeyCodeExchange> { self.pkce.as_ref() } } #[async_trait] impl TokenCredentialExecutor for OpenIdCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.app_config.authority); @@ -127,7 +127,7 @@ impl TokenCredentialExecutor for OpenIdCredential { } } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); @@ -297,16 +297,13 @@ impl OpenIdCredentialBuilder { self } - pub fn with_pkce( - &mut self, - proof_key_for_code_exchange: &ProofKeyForCodeExchange, - ) -> &mut Self { + pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self { self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); self } - pub fn generate_pkce(&mut self) -> AuthorizationResult<&mut Self> { - let pkce = ProofKeyForCodeExchange::generate()?; + pub fn generate_pkce(&mut self) -> IdentityResult<&mut Self> { + let pkce = ProofKeyCodeExchange::oneshot()?; self.with_code_verifier(pkce.code_verifier.as_str()); self.credential.pkce = Some(pkce); Ok(self) diff --git a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs b/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs deleted file mode 100644 index fc4af66b..00000000 --- a/graph-oauth/src/identity/credentials/proof_key_for_code_exchange.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::oauth::Crypto; -use graph_error::AuthorizationResult; - -#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct ProofKeyForCodeExchange { - /// The code verifier is not included in the authorization URL. - /// You can set the code verifier here and then use the From trait - /// for [AuthorizationCodeCredential] which does use the code verifier. - pub code_verifier: String, - /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). - /// Required if code_challenge_method is included. For more information, see the PKCE RFC. - /// This parameter is now recommended for all application types, both public and confidential - /// clients, and required by the Microsoft identity platform for single page apps using the - /// authorization code flow. - pub code_challenge: String, - /// The method used to encode the code_verifier for the code_challenge parameter. - /// This SHOULD be S256, but the spec allows the use of plain if the client can't support SHA256. - /// - /// If excluded, code_challenge is assumed to be plaintext if code_challenge is included. - /// The Microsoft identity platform supports both plain and S256. - /// For more information, see the PKCE RFC. This parameter is required for single page - /// apps using the authorization code flow. - pub code_challenge_method: String, -} - -impl ProofKeyForCodeExchange { - pub fn new<T: AsRef<str>>( - code_verifier: T, - code_challenge: T, - code_challenge_method: T, - ) -> ProofKeyForCodeExchange { - ProofKeyForCodeExchange { - code_verifier: code_verifier.as_ref().to_owned(), - code_challenge: code_challenge.as_ref().to_owned(), - code_challenge_method: code_challenge_method.as_ref().to_owned(), - } - } - - /// Generate a code challenge and code verifier for the - /// authorization code grant flow using proof key for - /// code exchange (PKCE) and SHA256. - /// - /// [ProofKeyForCodeExchange] contains a code_verifier, - /// code_challenge, and code_challenge_method for use in the authorization code grant. - /// - /// For authorization, the code_challenge_method parameter in the request body - /// is automatically set to 'S256'. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn generate() -> AuthorizationResult<ProofKeyForCodeExchange> { - let (code_verifier, code_challenge) = Crypto::sha256_secure_string()?; - Ok(ProofKeyForCodeExchange { - code_verifier, - code_challenge, - code_challenge_method: "S256".to_owned(), - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn pkce_generate() { - let pkce = ProofKeyForCodeExchange::generate().unwrap(); - assert_eq!(pkce.code_challenge.len(), 43); - } -} diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 8d0358d8..3d0d2022 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -6,7 +6,7 @@ use crate::identity::{ }; use crate::oauth::UnInitializedCredentialExecutor; use async_trait::async_trait; -use graph_error::{AuthExecutionResult, AuthorizationResult, AF}; +use graph_error::{AuthExecutionResult, IdentityResult, AF}; use graph_extensions::cache::{ InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, UnInitializedTokenStore, }; @@ -67,10 +67,6 @@ impl PublicClientApplication { #[async_trait] impl ClientApplication for PublicClientApplication { - fn client_application_type(&self) -> ClientApplicationType { - ClientApplicationType::ConfidentialClientApplication - } - fn get_token_silent(&mut self) -> AuthExecutionResult<String> { let cache_id = self.app_config().cache_id(); if self.is_store_and_token_initialized(cache_id.as_str()) { @@ -161,11 +157,11 @@ impl TokenStore for PublicClientApplication { #[async_trait] impl TokenCredentialExecutor for PublicClientApplication { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { self.credential.form_urlencode() } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index ebca8773..b5a5342a 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -2,7 +2,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use async_trait::async_trait; -use graph_error::{AuthorizationResult, AF}; +use graph_error::{IdentityResult, AF}; use std::collections::HashMap; use url::Url; use uuid::Uuid; @@ -68,7 +68,7 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); self.serializer .authority(&azure_cloud_instance, &self.app_config.authority); @@ -80,7 +80,7 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { Url::parse(uri.as_str()).map_err(AF::from) } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); @@ -186,7 +186,7 @@ impl ResourceOwnerPasswordCredentialBuilder { pub fn with_authority<T: Into<Authority>>( &mut self, authority: T, - ) -> AuthorizationResult<&mut Self> { + ) -> IdentityResult<&mut Self> { let authority = authority.into(); if vec![ Authority::Common, diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index e23fc805..0253dbe3 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -3,7 +3,7 @@ use crate::identity::{Authority, AzureCloudInstance}; use async_trait::async_trait; use dyn_clone::DynClone; -use graph_error::{AuthExecutionResult, AuthorizationResult}; +use graph_error::{AuthExecutionResult, IdentityResult}; use http::header::ACCEPT; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; @@ -20,9 +20,9 @@ pub trait TokenCredentialExecutor: DynClone { true } - fn uri(&mut self) -> AuthorizationResult<Url>; + fn uri(&mut self) -> IdentityResult<Url>; - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>>; + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; fn client_id(&self) -> &Uuid { &self.app_config().client_id @@ -50,7 +50,7 @@ pub trait TokenCredentialExecutor: DynClone { &self.app_config().extra_query_parameters } - fn openid_configuration_url(&self) -> AuthorizationResult<Url> { + fn openid_configuration_url(&self) -> IdentityResult<Url> { Ok(Url::parse( format!( "{}/{}/v2.0/.well-known/openid-configuration", @@ -204,11 +204,11 @@ impl TokenCredentialExecutor for UnInitializedCredentialExecutor { false } - fn uri(&mut self) -> AuthorizationResult<Url> { + fn uri(&mut self) -> IdentityResult<Url> { panic!("TokenCredentialExecutor is UnInitialized"); } - fn form_urlencode(&mut self) -> AuthorizationResult<HashMap<String, String>> { + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { panic!("TokenCredentialExecutor is UnInitialized"); } diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 71e2768d..fda286a7 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -30,12 +30,12 @@ //! # Example ConfidentialClientApplication Authorization Code Flow //! ```rust //! use url::Url; -//! use graph_error::AuthorizationResult; +//! use graph_error::IdentityResult; //! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! -//! pub fn authorization_url(client_id: &str) -> AuthorizationResult<Url> { +//! pub fn authorization_url(client_id: &str) -> IdentityResult<Url> { //! let auth_url_parameters = ConfidentialClientApplication::builder(client_id) -//! .authorization_code_url_builder() +//! .auth_code_authorization_url_builder() //! .with_redirect_uri("http://localhost:8000/redirect") //! .with_scope(vec!["user.read"]) //! .build(); @@ -73,11 +73,12 @@ pub mod oauth { pub use crate::auth::OAuthSerializer; pub use crate::discovery::graph_discovery; pub use crate::discovery::jwt_keys; - pub use crate::discovery::well_known; pub use crate::grants::GrantRequest; pub use crate::grants::GrantType; pub use crate::identity::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; - pub use graph_extensions::token::{IdToken, MsalToken}; + pub use graph_extensions::{ + crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, + }; } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index c8dda868..e8f4ab8a 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -12,6 +12,7 @@ use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; +use futures::TryFutureExt; use graph_http::api_impl::BearerToken; use std::sync::Mutex; @@ -24,6 +25,8 @@ lazy_static! { pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); } +//pub const APPLICATIONS_CLIENT: Mutex<Option<(String, Graph)>> = Mutex::new(OAuthTestClient::graph_by_rid(ResourceIdentity::Applications)); + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, AsFile, FromFile, Default)] pub enum TestEnv { AppVeyor, diff --git a/tests/discovery_tests.rs b/tests/discovery_tests.rs deleted file mode 100644 index 7e74fedf..00000000 --- a/tests/discovery_tests.rs +++ /dev/null @@ -1,104 +0,0 @@ -use graph_oauth::oauth::jwt_keys::JWTKeys; -use graph_oauth::oauth::{OAuthParameter, OAuthSerializer}; -use graph_rs_sdk::oauth::graph_discovery::{ - GraphDiscovery, MicrosoftSigningKeysV1, MicrosoftSigningKeysV2, -}; - -#[test] -fn graph_discovery_oauth_v1() { - let oauth: OAuthSerializer = GraphDiscovery::V1.oauth().unwrap(); - let keys: MicrosoftSigningKeysV1 = GraphDiscovery::V1.signing_keys().unwrap(); - assert_eq!( - oauth.get(OAuthParameter::AuthorizationUrl), - Some(keys.authorization_endpoint.to_string()) - ); - assert_eq!( - oauth.get(OAuthParameter::TokenUrl), - Some(keys.token_endpoint.to_string()) - ); - assert_eq!( - oauth.get(OAuthParameter::RefreshTokenUrl), - Some(keys.token_endpoint.to_string()) - ); - assert_eq!( - oauth.get(OAuthParameter::LogoutURL), - Some(keys.end_session_endpoint) - ); -} - -#[test] -fn graph_discovery_oauth_v2() { - let oauth: OAuthSerializer = GraphDiscovery::V2.oauth().unwrap(); - let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.signing_keys().unwrap(); - assert_eq!( - oauth.get(OAuthParameter::AuthorizationUrl), - Some(keys.authorization_endpoint) - ); - assert_eq!( - oauth.get(OAuthParameter::TokenUrl), - Some(keys.token_endpoint.to_string()) - ); - assert_eq!( - oauth.get(OAuthParameter::RefreshTokenUrl), - Some(keys.token_endpoint) - ); - assert_eq!( - oauth.get(OAuthParameter::LogoutURL), - Some(keys.end_session_endpoint) - ); -} - -#[tokio::test] -async fn async_graph_discovery_oauth_v2() { - let oauth: OAuthSerializer = GraphDiscovery::V2.async_oauth().await.unwrap(); - let keys: MicrosoftSigningKeysV2 = GraphDiscovery::V2.async_signing_keys().await.unwrap(); - assert_eq!( - oauth.get(OAuthParameter::AuthorizationUrl), - Some(keys.authorization_endpoint) - ); - assert_eq!( - oauth.get(OAuthParameter::TokenUrl), - Some(keys.token_endpoint.to_string()) - ); - assert_eq!( - oauth.get(OAuthParameter::RefreshTokenUrl), - Some(keys.token_endpoint) - ); - assert_eq!( - oauth.get(OAuthParameter::LogoutURL), - Some(keys.end_session_endpoint) - ); -} - -#[test] -fn jwt_keys() { - let keys = JWTKeys::discovery().unwrap(); - assert!(!keys.keys().is_empty()); - - for key in keys.into_iter() { - assert!(key.kty.is_some()); - } -} - -#[tokio::test] -async fn async_jwt_keys() { - let keys = JWTKeys::async_discovery().await.unwrap(); - assert!(!keys.keys().is_empty()); - - for key in keys.into_iter() { - assert!(key.kty.is_some()); - } -} - -#[test] -fn tenant_signing_keys() { - if let Ok(tenant) = std::env::var("TEST_APP_TENANT") { - let keys: MicrosoftSigningKeysV2 = GraphDiscovery::Tenant(tenant.to_string()) - .signing_keys() - .unwrap(); - assert_eq!( - keys.authorization_endpoint, - format!("https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize") - ); - } -} diff --git a/tests/grants_authorization_code.rs b/tests/grants_authorization_code.rs deleted file mode 100644 index efd8be8f..00000000 --- a/tests/grants_authorization_code.rs +++ /dev/null @@ -1,120 +0,0 @@ -use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, MsalToken, OAuthSerializer}; -use test_tools::oauth::OAuthTestTool; -use url::{Host, Url}; - -#[test] -pub fn authorization_url() { - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") - .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") - .response_type("code") - .redirect_uri("http://localhost:8080") - .response_mode("query") - .response_type("code") - .add_scope("Read.Write") - .state("12345") - .prompt("login") - .code_challenge_method("plain") - .code_challenge("code_challenge") - .domain_hint("consumers"); - - let url = oauth - .encode_uri(GrantType::AuthorizationCode, GrantRequest::Authorization) - .unwrap(); - let test_url = - "https://login.microsoftonline.com/common/oauth2/authorize?client_id=6731de76-14a6-49ae-97bc-6eba6914391e&redirect_uri=http%3A%2F%2Flocalhost%3A8080&state=12345&response_mode=query&response_type=code&scope=Read.Write&prompt=login&domain_hint=consumers&code_challenge=code_challenge&code_challenge_method=plain"; - let parsed_url = Url::parse(url.as_str()).unwrap(); - - assert_eq!(parsed_url.scheme(), "https"); - assert_eq!( - parsed_url.host(), - Some(Host::Domain("login.microsoftonline.com")) - ); - assert_eq!(test_url, url); -} - -#[test] -fn access_token_uri() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .client_secret("CLDIE3F") - .redirect_uri("http://localhost:8888/redirect") - .grant_type("authorization_code") - .add_scope("Read.Write") - .add_scope("Fall.Down") - .authorization_code("11201a230923f-4259-a230011201a230923f") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .code_verifier("bb301aaab3011201a230923f-4259-a230923fds32"); - let test_url = - "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL&scope=Fall.Down+Read.Write&grant_type=authorization_code&code_verifier=bb301aaab3011201a230923f-4259-a230923fds32"; - let url = oauth - .encode_uri(GrantType::AuthorizationCode, GrantRequest::AccessToken) - .unwrap(); - assert_eq!(test_url, url); -} - -#[test] -fn refresh_token_uri() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .client_secret("CLDIE3F") - .redirect_uri("http://localhost:8888/redirect") - .grant_type("refresh_token") - .add_scope("Read.Write") - .add_scope("Fall.Down") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - - let mut access_token = MsalToken::new( - "access_token", - 3600, - "asfasf", - vec!["Read.Write", "Fall.Down"], - ); - access_token.with_refresh_token("32LKLASDKJ"); - oauth.access_token(access_token); - - let body = oauth - .encode_uri(GrantType::AuthorizationCode, GrantRequest::RefreshToken) - .unwrap(); - let test_url = - "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&refresh_token=32LKLASDKJ&grant_type=refresh_token&scope=Fall.Down+Read.Write"; - assert_eq!(test_url, access_token.get_token); -} - -#[test] -pub fn access_token_body_contains() { - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") - .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") - .redirect_uri("http://localhost:8080") - .add_scope("Read.Write") - .response_mode("query") - .response_type("code") - .state("12345") - .prompt("login") - .login_hint("value") - .domain_hint("consumers") - .code_challenge_method("plain") - .code_challenge("code_challenge") - .code_verifier("code_verifier") - .client_assertion("client_assertion") - .client_assertion_type("client_assertion_type") - .session_state("session_state") - .logout_url("https://login.live.com/oauth20_logout.srf?") - .post_logout_redirect_uri("http://localhost:8000/redirect"); - - let vec_included = - GrantType::AuthorizationCode.available_credentials(GrantRequest::Authorization); - OAuthTestTool::oauth_contains_credentials(&mut oauth, &vec_included); - OAuthTestTool::oauth_query_uri_test( - &mut oauth, - GrantType::AuthorizationCode, - GrantRequest::Authorization, - vec_included, - ); -} diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs index 443b5826..c3888280 100644 --- a/tests/todo_tasks_request.rs +++ b/tests/todo_tasks_request.rs @@ -19,7 +19,6 @@ struct TodoListsTasks { #[tokio::test] async fn list_users() { - std::env::set_var("GRAPH_TEST_ENV", "true"); let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::TodoListsTasks).await From 2e1aa6d1760a101b3197f8fc061710484c225990 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 11 Oct 2023 23:19:20 -0400 Subject: [PATCH 042/118] Refactor ConfidentialClientApplication and PublicClientApplication to use generic and have token caches in credentials --- graph-extensions/src/cache/cache_store.rs | 0 graph-oauth/src/identity/cache/in_memory_client_store.rs | 0 graph-oauth/src/identity/cache/mod.rs | 0 .../src/identity/credentials/{ => legacy}/implicit_credential.rs | 0 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 graph-extensions/src/cache/cache_store.rs create mode 100644 graph-oauth/src/identity/cache/in_memory_client_store.rs create mode 100644 graph-oauth/src/identity/cache/mod.rs rename graph-oauth/src/identity/credentials/{ => legacy}/implicit_credential.rs (100%) diff --git a/graph-extensions/src/cache/cache_store.rs b/graph-extensions/src/cache/cache_store.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-oauth/src/identity/cache/in_memory_client_store.rs b/graph-oauth/src/identity/cache/in_memory_client_store.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-oauth/src/identity/cache/mod.rs b/graph-oauth/src/identity/cache/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-oauth/src/identity/credentials/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs similarity index 100% rename from graph-oauth/src/identity/credentials/implicit_credential.rs rename to graph-oauth/src/identity/credentials/legacy/implicit_credential.rs From 74089fd6bab9eee2e005b41454012b1aeef81b9c Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 11 Oct 2023 23:19:27 -0400 Subject: [PATCH 043/118] Refactor ConfidentialClientApplication and PublicClientApplication to use generic and have token caches in credentials --- Cargo.toml | 1 + graph-error/src/graph_failure.rs | 43 ++- graph-extensions/src/cache/cache_store.rs | 42 +++ .../src/cache/in_memory_credential_store.rs | 57 +-- graph-extensions/src/cache/mod.rs | 2 + .../src/token/client_application.rs | 4 +- .../src/blocking/blocking_request_handler.rs | 5 +- graph-http/src/client.rs | 45 +-- graph-http/src/request_handler.rs | 98 ++++-- graph-oauth/Cargo.toml | 2 + graph-oauth/src/auth.rs | 21 +- graph-oauth/src/discovery/jwt_keys.rs | 3 +- .../src/identity/allowed_host_validator.rs | 1 + .../src/identity/application_options.rs | 8 +- .../identity/authorization_query_response.rs | 3 +- .../src/identity/authorization_serializer.rs | 7 +- .../identity/cache/in_memory_client_store.rs | 9 + graph-oauth/src/identity/cache/mod.rs | 3 + .../src/identity/credentials/app_config.rs | 76 +++- .../credentials/application_builder.rs | 51 ++- .../auth_code_authorization_url.rs | 132 ++++--- ...thorization_code_certificate_credential.rs | 45 +-- .../authorization_code_credential.rs | 45 ++- .../client_assertion_credential.rs | 26 +- .../client_certificate_credential.rs | 22 +- .../client_credentials_authorization_url.rs | 25 +- .../credentials/client_secret_credential.rs | 84 ++++- .../confidential_client_application.rs | 325 ++++++++++++------ .../credentials/device_code_credential.rs | 33 +- .../credentials/environment_credential.rs | 26 +- .../legacy/code_flow_authorization_url.rs | 8 +- .../legacy/code_flow_credential.rs | 9 +- .../credentials/legacy/implicit_credential.rs | 10 +- .../src/identity/credentials/legacy/mod.rs | 2 + .../legacy/token_flow_authorization_url.rs | 8 +- graph-oauth/src/identity/credentials/mod.rs | 55 ++- .../credentials/open_id_authorization_url.rs | 213 ++++++------ .../credentials/open_id_credential.rs | 68 ++-- .../src/identity/credentials/prompt.rs | 3 +- .../credentials/public_client_application.rs | 78 +++-- .../resource_owner_password_credential.rs | 27 +- .../src/identity/credentials/response_type.rs | 3 +- .../credentials/token_credential_executor.rs | 72 ++-- .../src/identity/credentials/token_request.rs | 6 +- .../identity/credentials/x509_certificate.rs | 3 +- graph-oauth/src/identity/device_code.rs | 3 +- graph-oauth/src/identity/mod.rs | 29 +- graph-oauth/src/jwt.rs | 13 +- graph-oauth/src/lib.rs | 13 +- graph-oauth/src/oauth_error.rs | 8 +- src/client/graph.rs | 11 + test-tools/src/oauth_request.rs | 1 - 52 files changed, 1177 insertions(+), 710 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0769a301..705a0832 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ graph-http = { path = "./graph-http", version = "1.1.0", default-features=false graph-error = { path = "./graph-error", version = "0.2.2" } graph-core = { path = "./graph-core", version = "0.4.0" } graph-extensions = { path = "./graph-extensions", version = "0.1.0", default-features=false } +uuid = { version = "1.4.1", features = ["v4"] } # When updating or adding new features to this or dependent crates run # cargo tree -e features -i graph-rs-sdk diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index 684f1c67..03203cc0 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -1,12 +1,13 @@ use crate::download::AsyncDownloadError; use crate::internal::GraphRsError; -use crate::ErrorMessage; +use crate::{AuthExecutionError, AuthorizationFailure, ErrorMessage}; use reqwest::header::HeaderMap; use std::cell::BorrowMutError; use std::io; use std::io::ErrorKind; use std::str::Utf8Error; use std::sync::mpsc; +use url::form_urlencoded::parse; #[derive(Debug, thiserror::Error)] #[allow(clippy::large_enum_variant)] @@ -59,8 +60,9 @@ pub enum GraphFailure { )] PreFlightError { url: Option<reqwest::Url>, - headers: HeaderMap, - error: Box<GraphFailure>, + headers: Option<HeaderMap>, + error: Option<Box<GraphFailure>>, + message: String, }, #[error("{0:#?}", message)] @@ -108,3 +110,38 @@ impl From<ring::error::Unspecified> for GraphFailure { GraphFailure::CryptoError } } + +impl From<AuthExecutionError> for GraphFailure { + fn from(value: AuthExecutionError) -> Self { + match value { + AuthExecutionError::AuthorizationFailure(authorizationFailure) => { + match authorizationFailure { + AuthorizationFailure::RequiredValue { name, message } => { + GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message: format!("name: {:#?}, message: {:#?}", name, message), + } + } + AuthorizationFailure::UrlParseError(e) => GraphFailure::UrlParseError(e), + AuthorizationFailure::UuidError(uuidError) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message: "Client Id is not a valid UUID".to_owned(), + }, + AuthorizationFailure::Unknown(message) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message, + }, + } + } + AuthExecutionError::RequestError(e) => GraphFailure::ReqwestError(e), + AuthExecutionError::SerdeError(e) => GraphFailure::SerdeError(e), + AuthExecutionError::HttpError(e) => GraphFailure::HttpError(e), + } + } +} diff --git a/graph-extensions/src/cache/cache_store.rs b/graph-extensions/src/cache/cache_store.rs index e69de29b..27006504 100644 --- a/graph-extensions/src/cache/cache_store.rs +++ b/graph-extensions/src/cache/cache_store.rs @@ -0,0 +1,42 @@ +use crate::token::MsalToken; +use async_trait::async_trait; +use graph_error::AuthExecutionError; + +pub trait AsBearer<RHS = Self> { + fn as_bearer(&self) -> String; +} + +pub struct BearerToken(String); + +impl AsBearer for BearerToken { + fn as_bearer(&self) -> String { + self.0.clone() + } +} + +impl AsBearer for String { + fn as_bearer(&self) -> String { + self.clone() + } +} + +impl AsBearer for &str { + fn as_bearer(&self) -> String { + self.to_string() + } +} + +impl AsBearer for MsalToken { + fn as_bearer(&self) -> String { + self.access_token.to_string() + } +} + +#[async_trait] +pub trait TokenCacheStore { + type Token: AsBearer; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError>; + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError>; +} diff --git a/graph-extensions/src/cache/in_memory_credential_store.rs b/graph-extensions/src/cache/in_memory_credential_store.rs index b8470752..d50ba970 100644 --- a/graph-extensions/src/cache/in_memory_credential_store.rs +++ b/graph-extensions/src/cache/in_memory_credential_store.rs @@ -1,54 +1,27 @@ -use crate::cache::{StoredToken, TokenStore, TokenStoreProvider}; +use crate::cache::{AsBearer, StoredToken, TokenStore, TokenStoreProvider}; use std::collections::HashMap; +use std::hash::Hash; +use std::sync::{Arc, RwLock}; #[derive(Clone)] -pub struct InMemoryCredentialStore { - store: HashMap<String, StoredToken>, +pub struct InMemoryCredentialStore<Token: AsBearer + Clone> { + store: Arc<RwLock<HashMap<String, Token>>>, } -impl InMemoryCredentialStore { - pub fn new(id: String, stored_token: StoredToken) -> InMemoryCredentialStore { - let mut store = HashMap::new(); - store.insert(id, stored_token); - - InMemoryCredentialStore { store } - } -} - -impl TokenStore for InMemoryCredentialStore { - fn token_store_provider(&self) -> TokenStoreProvider { - TokenStoreProvider::InMemory - } - - fn is_stored_token_initialized(&self, id: &str) -> bool { - if let Some(stored_token) = self.store.get(id) { - stored_token.is_initialized() - } else { - false +impl<Token: AsBearer + Clone> InMemoryCredentialStore<Token> { + pub fn new() -> InMemoryCredentialStore<Token> { + InMemoryCredentialStore { + store: Default::default(), } } - fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { - self.store.get(id) + pub fn store<T: Into<String>>(&mut self, cache_id: T, token: Token) { + let mut store = self.store.write().unwrap(); + store.insert(cache_id.into(), token); } - fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { - self.store.insert(id.to_string(), stored_token) - } - - fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { - if let Some(stored_token) = self.store.get(id) { - stored_token.get_bearer_token() - } else { - None - } - } - - fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { - if let Some(stored_token) = self.store.get(id) { - stored_token.get_refresh_token() - } else { - None - } + pub fn get(&self, cache_id: &str) -> Option<Token> { + let mut store = self.store.read().unwrap(); + store.get(cache_id).cloned() } } diff --git a/graph-extensions/src/cache/mod.rs b/graph-extensions/src/cache/mod.rs index 9fa05f58..b33d2b66 100644 --- a/graph-extensions/src/cache/mod.rs +++ b/graph-extensions/src/cache/mod.rs @@ -1,8 +1,10 @@ +mod cache_store; mod in_memory_credential_store; mod token_store; mod token_store_providers; mod token_watch_task; +pub use cache_store::*; pub use in_memory_credential_store::*; use std::fmt::{Debug, Formatter}; pub use token_store::*; diff --git a/graph-extensions/src/token/client_application.rs b/graph-extensions/src/token/client_application.rs index 185e0632..fa90a324 100644 --- a/graph-extensions/src/token/client_application.rs +++ b/graph-extensions/src/token/client_application.rs @@ -12,10 +12,8 @@ pub enum ClientApplicationType { dyn_clone::clone_trait_object!(ClientApplication); #[async_trait] -pub trait ClientApplication: TokenStore + DynClone { +pub trait ClientApplication: DynClone { fn get_token_silent(&mut self) -> AuthExecutionResult<String>; async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; - - fn get_stored_application_token(&mut self) -> Option<&StoredToken>; } diff --git a/graph-http/src/blocking/blocking_request_handler.rs b/graph-http/src/blocking/blocking_request_handler.rs index 2a6b9a11..134b99c2 100644 --- a/graph-http/src/blocking/blocking_request_handler.rs +++ b/graph-http/src/blocking/blocking_request_handler.rs @@ -31,8 +31,9 @@ impl BlockingRequestHandler { if let Some(err) = err { error = Some(GraphFailure::PreFlightError { url: Some(request_components.url.clone()), - headers: request_components.headers.clone(), - error: Box::new(err), + headers: Some(request_components.headers.clone()), + error: Some(Box::new(err)), + message: String::from("N/A"), }); } diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index a84bf2a2..6b1f3237 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,8 +1,9 @@ use crate::blocking::BlockingClient; use async_trait::async_trait; use graph_error::AuthExecutionResult; -use graph_extensions::cache::{StoredToken, TokenStore, TokenStoreProvider}; +use graph_extensions::cache::{StoredToken, TokenCacheStore, TokenStore, TokenStoreProvider}; use graph_extensions::token::ClientApplication; +use graph_oauth::identity::{ConfidentialClient, TokenCredentialExecutor}; use graph_oauth::oauth::{ConfidentialClientApplication, PublicClientApplication}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; @@ -59,10 +60,6 @@ impl ClientApplication for BearerToken { async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { Ok(self.0.clone()) } - - fn get_stored_application_token(&mut self) -> Option<&StoredToken> { - None - } } #[derive(Clone)] @@ -136,19 +133,23 @@ impl GraphClientConfiguration { self } - pub fn confidential_client_application( + pub fn confidential_client_application< + Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, + >( mut self, - confidential_client: ConfidentialClientApplication, + confidential_client: ConfidentialClient<Credential>, ) -> Self { self.config.client_application = Some(Box::new(confidential_client)); self } + /* pub fn public_client_application(mut self, public_client: PublicClientApplication) -> Self { self.config.client_application = Some(Box::new(public_client)); self } + */ pub fn default_headers(mut self, headers: HeaderMap) -> GraphClientConfiguration { for (key, value) in headers.iter() { self.config.headers.insert(key, value.clone()); @@ -313,14 +314,6 @@ impl Client { pub fn headers(&self) -> &HeaderMap { &self.headers } - - pub fn get_token(&mut self) -> Option<String> { - self.client_application.get_token_silent().ok() - } - - pub fn get_token2(&mut self) -> Option<&StoredToken> { - self.client_application.get_stored_application_token() - } } impl Default for Client { @@ -351,8 +344,10 @@ impl From<PublicClientApplication> for Client { } } -impl From<ConfidentialClientApplication> for Client { - fn from(value: ConfidentialClientApplication) -> Self { +impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static> + From<ConfidentialClient<Credential>> for Client +{ + fn from(value: ConfidentialClient<Credential>) -> Self { Client::new(value) } } @@ -382,20 +377,4 @@ mod test { let user_agent_header = client.builder.config.headers.get(USER_AGENT).unwrap(); assert_eq!("user_agent", user_agent_header.to_str().unwrap()); } - - #[test] - #[should_panic] - fn initialize_confidential_client() { - let mut client = GraphClientConfiguration::new() - .access_token("access_token") - .user_agent(HeaderValue::from_static("user_agent")) - .client_application( - ConfidentialClientApplication::builder("client-id") - .with_client_secret("secret") - .build(), - ) - .build(); - - assert!(client.client_application.get_stored_token("").is_none()); - } } diff --git a/graph-http/src/request_handler.rs b/graph-http/src/request_handler.rs index f8b4b6ac..d6931567 100644 --- a/graph-http/src/request_handler.rs +++ b/graph-http/src/request_handler.rs @@ -5,7 +5,7 @@ use crate::internal::{ }; use async_stream::try_stream; use futures::Stream; -use graph_error::{ErrorMessage, GraphFailure, GraphResult}; +use graph_error::{AuthExecutionResult, ErrorMessage, GraphFailure, GraphResult}; use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE}; use serde::de::DeserializeOwned; use std::collections::VecDeque; @@ -15,8 +15,7 @@ use url::Url; #[derive(Default)] pub struct RequestHandler { - pub(crate) inner: reqwest::Client, - pub(crate) access_token: String, + pub(crate) inner: Client, pub(crate) request_components: RequestComponents, pub(crate) error: Option<GraphFailure>, pub(crate) body: Option<BodyRead>, @@ -30,10 +29,8 @@ impl RequestHandler { err: Option<GraphFailure>, body: Option<BodyRead>, ) -> RequestHandler { - let mut token = inner.clone(); - let access_token = token.get_token().unwrap(); - - let mut original_headers = inner.headers; + let client_builder = inner.builder.clone(); + let mut original_headers = inner.headers.clone(); original_headers.extend(request_components.headers.clone()); request_components.headers = original_headers; @@ -41,26 +38,24 @@ impl RequestHandler { if let Some(err) = err { error = Some(GraphFailure::PreFlightError { url: Some(request_components.url.clone()), - headers: request_components.headers.clone(), - error: Box::new(err), + headers: Some(request_components.headers.clone()), + error: Some(Box::new(err)), + message: String::from("N/A"), }); } RequestHandler { - inner: inner.inner.clone(), - access_token, + inner: inner.clone(), request_components, error, body, - client_builder: inner.builder, + client_builder, } } pub fn into_blocking(self) -> BlockingRequestHandler { BlockingRequestHandler::new( - self.client_builder - .access_token(self.access_token) - .build_blocking(), + self.client_builder.build_blocking(), self.request_components, self.error, self.body, @@ -157,14 +152,23 @@ impl RequestHandler { Paging(self) } - pub(crate) fn default_request_builder(&mut self) -> reqwest::RequestBuilder { + pub(crate) async fn default_request_builder_with_token( + &mut self, + ) -> AuthExecutionResult<(String, reqwest::RequestBuilder)> { + let access_token = self + .inner + .client_application + .get_token_silent_async() + .await?; + let request_builder = self + .inner .inner .request( self.request_components.method.clone(), self.request_components.url.clone(), ) - .bearer_auth(self.access_token.as_str()) + .bearer_auth(access_token.as_str()) .headers(self.request_components.headers.clone()); if let Some(body) = self.body.take() { @@ -172,25 +176,57 @@ impl RequestHandler { .headers .entry(CONTENT_TYPE) .or_insert(HeaderValue::from_static("application/json")); - return request_builder + return Ok(( + access_token, + request_builder + .body::<reqwest::Body>(body.into()) + .headers(self.request_components.headers.clone()), + )); + } + Ok((access_token, request_builder)) + } + + pub(crate) async fn default_request_builder(&mut self) -> GraphResult<reqwest::RequestBuilder> { + let access_token = self + .inner + .client_application + .get_token_silent_async() + .await?; + + let request_builder = self + .inner + .inner + .request( + self.request_components.method.clone(), + self.request_components.url.clone(), + ) + .bearer_auth(access_token.as_str()) + .headers(self.request_components.headers.clone()); + + if let Some(body) = self.body.take() { + self.request_components + .headers + .entry(CONTENT_TYPE) + .or_insert(HeaderValue::from_static("application/json")); + return Ok(request_builder .body::<reqwest::Body>(body.into()) - .headers(self.request_components.headers.clone()); + .headers(self.request_components.headers.clone())); } - request_builder + Ok(request_builder) } /// Builds the request and returns a [`reqwest::RequestBuilder`]. #[inline] - pub fn build(mut self) -> GraphResult<reqwest::RequestBuilder> { + pub async fn build(mut self) -> GraphResult<reqwest::RequestBuilder> { if let Some(err) = self.error { return Err(err); } - Ok(self.default_request_builder()) + self.default_request_builder().await } #[inline] pub async fn send(self) -> GraphResult<reqwest::Response> { - let request_builder = self.build()?; + let request_builder = self.build().await?; request_builder.send().await.map_err(GraphFailure::from) } } @@ -273,7 +309,7 @@ impl Paging { return Err(err); } - let request = self.0.default_request_builder(); + let (access_token, request) = self.0.default_request_builder_with_token().await?; let response = request.send().await?; let (next, http_response) = Paging::http_response(response).await?; @@ -281,8 +317,7 @@ impl Paging { let mut vec = VecDeque::new(); vec.push_back(http_response); - let client = self.0.inner.clone(); - let access_token = self.0.access_token.clone(); + let client = self.0.inner.inner.clone(); while let Some(next) = next_link { let response = client .get(next) @@ -303,7 +338,7 @@ impl Paging { mut self, ) -> impl Stream<Item = PagingResult<T>> + 'a { try_stream! { - let request = self.0.default_request_builder(); + let (access_token, request) = self.0.default_request_builder_with_token().await?; let response = request.send().await?; let (next, http_response) = Paging::http_response(response).await?; let mut next_link = next; @@ -311,9 +346,10 @@ impl Paging { while let Some(url) = next_link { let response = self.0 + .inner .inner .get(url) - .bearer_auth(self.0.access_token.as_str()) + .bearer_auth(access_token.as_str()) .send() .await?; let (next, http_response) = Paging::http_response(response).await?; @@ -457,7 +493,7 @@ impl Paging { ) -> GraphResult<tokio::sync::mpsc::Receiver<PagingResult<T>>> { let (sender, receiver) = tokio::sync::mpsc::channel(buffer); - let request = self.0.default_request_builder(); + let (access_token, request) = self.0.default_request_builder_with_token().await?; let response = request.send().await?; let (next, http_response) = Paging::http_response(response).await?; let mut next_link = next; @@ -466,9 +502,7 @@ impl Paging { .await .unwrap(); - let client = self.0.inner.clone(); - let access_token = self.0.access_token.clone(); - + let client = self.0.inner.inner.clone(); tokio::spawn(async move { while let Some(next) = next_link { let result = diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index dc912aa9..aa5202b1 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -42,6 +42,8 @@ pretty_env_logger = "0.4" tokio = { version = "1.27.0", features = ["full"] } hyper = { version = "1.0.0-rc.3", features = ["full"] } http-body-util = "0.1.0-rc.2" +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } graph-error = { path = "../graph-error" } graph-extensions = { path = "../graph-extensions" } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 81946c41..e53e2938 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -1,20 +1,23 @@ -use crate::grants::{GrantRequest, GrantType}; -use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; -use crate::oauth::ResponseType; -use crate::oauth_error::OAuthError; -use crate::strum::IntoEnumIterator; -use base64::Engine; -use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; -use graph_extensions::token::{IdToken, MsalToken}; -use ring::rand::SecureRandom; use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::{BTreeSet, HashMap}; use std::default::Default; use std::fmt; use std::marker::PhantomData; + +use base64::Engine; +use ring::rand::SecureRandom; use url::form_urlencoded::Serializer; use url::Url; +use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; +use graph_extensions::token::{IdToken, MsalToken}; + +use crate::grants::{GrantRequest, GrantType}; +use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; +use crate::oauth::ResponseType; +use crate::oauth_error::OAuthError; +use crate::strum::IntoEnumIterator; + /// Fields that represent common OAuth credentials. #[derive( Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, diff --git a/graph-oauth/src/discovery/jwt_keys.rs b/graph-oauth/src/discovery/jwt_keys.rs index 41da1987..e8bca78b 100644 --- a/graph-oauth/src/discovery/jwt_keys.rs +++ b/graph-oauth/src/discovery/jwt_keys.rs @@ -1,6 +1,7 @@ -use graph_error::GraphResult; use std::collections::HashMap; +use graph_error::GraphResult; + #[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct Keys { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index d3732be1..00736ff5 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; use std::hash::Hash; + use url::{Host, Url}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 7aaf5e1c..4f0aaf80 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -1,8 +1,9 @@ -use crate::identity::AadAuthorityAudience; -use crate::oauth::AzureCloudInstance; use url::Url; use uuid::Uuid; +use crate::identity::AadAuthorityAudience; +use crate::oauth::AzureCloudInstance; + /// Application Options typically stored as JSON file in .net applications. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] pub struct ApplicationOptions { @@ -51,9 +52,10 @@ impl ApplicationOptions { #[cfg(test)] mod test { - use super::*; use std::fs::File; + use super::*; + #[test] fn application_options_from_file() { let file = diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index f4602af2..dfda56ae 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,6 +1,7 @@ -use serde_json::Value; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; + +use serde_json::Value; use url::Url; /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-31#section-4.2.2.1 diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_serializer.rs index 1493477b..88008b95 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_serializer.rs @@ -1,8 +1,11 @@ -use crate::identity::AzureCloudInstance; -use graph_error::IdentityResult; use std::collections::HashMap; + use url::Url; +use graph_error::IdentityResult; + +use crate::identity::AzureCloudInstance; + pub trait AuthorizationSerializer { fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url>; fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; diff --git a/graph-oauth/src/identity/cache/in_memory_client_store.rs b/graph-oauth/src/identity/cache/in_memory_client_store.rs index e69de29b..609f994e 100644 --- a/graph-oauth/src/identity/cache/in_memory_client_store.rs +++ b/graph-oauth/src/identity/cache/in_memory_client_store.rs @@ -0,0 +1,9 @@ +use std::sync::{Arc, RwLock}; + +use crate::oauth::TokenCredentialExecutor; + +#[derive(Clone)] +pub struct InMemoryClientStore<Client: TokenCredentialExecutor, Token> { + client: Box<Client>, + token: Arc<RwLock<Token>>, +} diff --git a/graph-oauth/src/identity/cache/mod.rs b/graph-oauth/src/identity/cache/mod.rs index e69de29b..05b040aa 100644 --- a/graph-oauth/src/identity/cache/mod.rs +++ b/graph-oauth/src/identity/cache/mod.rs @@ -0,0 +1,3 @@ +pub use in_memory_client_store::*; + +mod in_memory_client_store; diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 94ff6875..358502fd 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,10 +1,12 @@ -use crate::identity::{Authority, AzureCloudInstance}; -use graph_extensions::cache::{TokenStore, UnInitializedTokenStore}; -use reqwest::header::HeaderMap; +use base64::Engine; use std::collections::HashMap; + +use reqwest::header::HeaderMap; use url::Url; use uuid::Uuid; +use crate::identity::{Authority, AzureCloudInstance}; + #[derive(Clone, Debug, Default, PartialEq)] pub struct AppConfig { /// The directory tenant that you want to request permission from. @@ -25,30 +27,71 @@ pub struct AppConfig { /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. pub(crate) redirect_uri: Option<Url>, + pub(crate) cache_id: String, } impl AppConfig { pub(crate) fn new() -> AppConfig { + let client_id = Uuid::default(); + let cache_id = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()); + AppConfig { tenant_id: None, - client_id: Uuid::default(), + client_id, authority: Default::default(), azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + cache_id, + } + } + + pub(crate) fn new_init( + client_id: Uuid, + tenant: Option<impl AsRef<str>>, + redirect_uri: Option<Url>, + ) -> AppConfig { + let tenant_id: Option<String> = tenant.map(|value| value.as_ref().to_string()); + let cache_id = { + if let Some(tenant_id) = tenant_id.as_ref() { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + client_id.to_string() + )) + } else { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()) + } + }; + + AppConfig { + tenant_id, + client_id, + authority: Default::default(), + azure_cloud_instance: Default::default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri, + cache_id, } } pub(crate) fn new_with_client_id(client_id: impl AsRef<str>) -> AppConfig { + let client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); + let cache_id = + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()); + AppConfig { tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + client_id, authority: Default::default(), azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + cache_id, } } @@ -56,22 +99,35 @@ impl AppConfig { tenant_id: impl AsRef<str>, client_id: impl AsRef<str>, ) -> AppConfig { + let client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); + let tenant_id = tenant_id.as_ref(); + let cache_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + client_id.to_string() + )); + AppConfig { - tenant_id: Some(tenant_id.as_ref().to_string()), - client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - authority: Authority::TenantId(tenant_id.as_ref().to_string()), + tenant_id: Some(tenant_id.to_string()), + client_id, + authority: Authority::TenantId(tenant_id.to_string()), azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + cache_id, } } pub(crate) fn cache_id(&self) -> String { if let Some(tenant_id) = self.tenant_id.as_ref() { - format!("{},{}", tenant_id, self.client_id.to_string()) + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + self.client_id.to_string() + )) } else { - self.client_id.to_string() + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.client_id.to_string()) } } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 7a02836c..bb867a5d 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,3 +1,13 @@ +use base64::Engine; +use std::collections::HashMap; +use std::env::VarError; + +use http::{HeaderMap, HeaderName, HeaderValue}; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; + use crate::identity::{ application_options::ApplicationOptions, credentials::app_config::AppConfig, credentials::client_assertion_credential::ClientAssertionCredentialBuilder, @@ -5,18 +15,11 @@ use crate::identity::{ AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, - OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredential, - ResourceOwnerPasswordCredentialBuilder, + OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredentialBuilder, }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; use crate::oauth::OpenIdAuthorizationUrlBuilder; -use graph_error::{IdentityResult, AF}; -use http::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; -use std::env::VarError; -use url::Url; -use uuid::Uuid; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { @@ -223,6 +226,18 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { "Both represent an authority audience and cannot be set at the same time", )?; + let cache_id = { + if let Some(tenant_id) = value.tenant_id.as_ref() { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + value.client_id.to_string() + )) + } else { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value.client_id.to_string()) + } + }; + Ok(ConfidentialClientApplicationBuilder { app_config: AppConfig { tenant_id: value.tenant_id, @@ -235,6 +250,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + cache_id, }, }) } @@ -361,6 +377,18 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time", )?; + let cache_id = { + if let Some(tenant_id) = value.tenant_id.as_ref() { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + value.client_id.to_string() + )) + } else { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value.client_id.to_string()) + } + }; + Ok(PublicClientApplicationBuilder { app_config: AppConfig { tenant_id: value.tenant_id, @@ -373,6 +401,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), redirect_uri: None, + cache_id, }, }) } @@ -380,11 +409,13 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { #[cfg(test)] mod test { - use super::*; - use crate::identity::AadAuthorityAudience; use http::header::AUTHORIZATION; use http::HeaderValue; + use crate::identity::AadAuthorityAudience; + + use super::*; + #[test] #[should_panic] fn confidential_client_error_result_on_instance_and_aci() { diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index ef852cf3..4371de62 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,21 +1,22 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{ - Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Prompt, - ResponseMode, ResponseType, -}; - -use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; - -use graph_error::{IdentityResult, AF}; +use std::collections::BTreeSet; +use std::fmt::{Debug, Formatter}; -use crate::identity::credentials::app_config::AppConfig; -use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; use reqwest::IntoUrl; -use std::collections::BTreeSet; use url::form_urlencoded::Serializer; use url::Url; use uuid::Uuid; +use graph_error::{IdentityResult, AF}; +use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; +use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Prompt, + ResponseMode, ResponseType, +}; + /// Get the authorization url required to perform the initial authorization and redirect in the /// authorization code flow. /// @@ -48,7 +49,7 @@ use uuid::Uuid; /// .build(); /// } /// ``` -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct AuthCodeAuthorizationUrlParameters { pub(crate) app_config: AppConfig, pub(crate) response_type: BTreeSet<ResponseType>, @@ -67,14 +68,60 @@ pub struct AuthCodeAuthorizationUrlParameters { pub(crate) response_mode: Option<ResponseMode>, pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, + /// Required. + /// A space-separated list of scopes that you want the user to consent to. + /// For the /authorize leg of the request, this parameter can cover multiple resources. + /// This value allows your app to get consent for multiple web APIs you want to call. pub(crate) scope: Vec<String>, + /// Optional + /// Indicates the type of user interaction that is required. The only valid values at + /// this time are login, none, consent, and select_account. + /// + /// The [Prompt::Login] claim forces the user to enter their credentials on that request, + /// which negates single sign-on. + /// + /// The [Prompt::None] parameter is the opposite, and should be paired with a login_hint to + /// indicate which user must be signed in. These parameters ensure that the user isn't + /// presented with any interactive prompt at all. If the request can't be completed silently + /// via single sign-on, the Microsoft identity platform returns an error. Causes include no + /// signed-in user, the hinted user isn't signed in, or multiple users are signed in but no + /// hint was provided. + /// + /// The [Prompt::Consent] claim triggers the OAuth consent dialog after the + /// user signs in. The dialog asks the user to grant permissions to the app. + /// + /// Finally, [Prompt::SelectAccount] shows the user an account selector, negating silent SSO but + /// allowing the user to pick which account they intend to sign in with, without requiring + /// credential entry. You can't use both login_hint and select_account. pub(crate) prompt: Option<Prompt>, + /// Optional + /// The realm of the user in a federated directory. This skips the email-based discovery + /// process that the user goes through on the sign-in page, for a slightly more streamlined + /// user experience. For tenants that are federated through an on-premises directory + /// like AD FS, this often results in a seamless sign-in because of the existing login session. pub(crate) domain_hint: Option<String>, + /// Optional + /// You can use this parameter to pre-fill the username and email address field of the + /// sign-in page for the user, if you know the username ahead of time. Often, apps use + /// this parameter during re-authentication, after already extracting the login_hint + /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, pub(crate) code_challenge: Option<String>, pub(crate) code_challenge_method: Option<String>, } +impl Debug for AuthCodeAuthorizationUrlParameters { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthCodeAuthorizationUrlParameters") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .field("response_type", &self.response_type) + .field("response_mode", &self.response_mode) + .field("prompt", &self.prompt) + .finish() + } +} + impl AuthCodeAuthorizationUrlParameters { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, @@ -84,8 +131,8 @@ impl AuthCodeAuthorizationUrlParameters { response_type.insert(ResponseType::Code); let redirect_uri_result = Url::parse(redirect_uri.as_str()); - Ok(AuthCodeAuthorizationUrlParameters { - app_config: AppConfig { + /* + AppConfig { tenant_id: None, client_id: Uuid::try_parse(client_id.as_ref())?, authority: Default::default(), @@ -94,6 +141,14 @@ impl AuthCodeAuthorizationUrlParameters { extra_header_parameters: Default::default(), redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), }, + */ + + Ok(AuthCodeAuthorizationUrlParameters { + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + Some(redirect_uri.into_url().or(redirect_uri_result)?), + ), response_type, response_mode: None, nonce: None, @@ -174,9 +229,10 @@ impl AuthCodeAuthorizationUrlParameters { } mod web_view_authenticator { - use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; use graph_extensions::web::{InteractiveAuthenticator, InteractiveWebView, WebViewOptions}; + use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; + impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { fn interactive_authentication( &self, @@ -469,11 +525,10 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - /// Set the required permissions for the authorization request. - /// - /// Providing a scope of `id_token` automatically adds [ResponseType::IdToken] - /// and generates a secure nonce value. - /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] + /// Required. + /// A space-separated list of scopes that you want the user to consent to. + /// For the /authorize leg of the request, this parameter can cover multiple resources. + /// This value allows your app to get consent for multiple web APIs you want to call. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { self.parameters.scope.extend( scope @@ -481,51 +536,18 @@ impl AuthCodeAuthorizationUrlParameterBuilder { .map(|s| s.to_string()) .map(|s| s.trim().to_owned()), ); - - if self.parameters.nonce.is_none() - && self.parameters.scope.contains(&String::from("id_token")) - { - let _ = self.with_id_token_scope(); - } self } - /// Automatically adds `profile` and `email` to the scope parameter. - /// - /// If you need a refresh token then include `offline_access` as a scope. - /// The `offline_access` scope is not included here. - pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { - self.parameters - .scope - .extend(vec!["profile".to_owned(), "email".to_owned()]); - Ok(self) - } - /// Adds the `offline_access` scope parameter which tells the authorization server /// to include a refresh token in the redirect uri query. - pub fn with_refresh_token_scope(&mut self) -> &mut Self { + pub fn with_offline_access(&mut self) -> &mut Self { self.parameters .scope .extend(vec!["offline_access".to_owned()]); self } - /// Adds the `id_token` scope parameter which tells the authorization server - /// to include an id token in the redirect uri query. - /// - /// Including the `id_token` scope also adds the id_token response type - /// and adds the `openid` scope parameter. - /// - /// Including `id_token` also requires a nonce parameter. - /// This is generated automatically. - /// See [AuthCodeAuthorizationUrlParameterBuilder::with_nonce_generated] - fn with_id_token_scope(&mut self) -> anyhow::Result<&mut Self> { - self.with_nonce_generated()?; - self.parameters.response_type.extend(ResponseType::IdToken); - self.parameters.scope.extend(vec!["id_token".to_owned()]); - Ok(self) - } - /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 463f3b8f..9eff37c9 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -1,3 +1,14 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::IntoUrl; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; + use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ @@ -5,14 +16,6 @@ use crate::identity::{ AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; -use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; -use http::{HeaderMap, HeaderName, HeaderValue}; -use reqwest::IntoUrl; -use std::collections::HashMap; -use url::Url; -use uuid::Uuid; - #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; @@ -27,7 +30,7 @@ credential_builder!( /// identity platform) back to your application. For example, a web browser, desktop, or mobile /// application operated by a user to sign in to your app and access their data. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct AuthorizationCodeCertificateCredential { pub(crate) app_config: AppConfig, /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. @@ -55,6 +58,14 @@ pub struct AuthorizationCodeCertificateCredential { serializer: OAuthSerializer, } +impl Debug for AuthorizationCodeCertificateCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthorizationCodeCertificateCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} impl AuthorizationCodeCertificateCredential { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, @@ -70,18 +81,12 @@ impl AuthorizationCodeCertificateCredential { } }; - let app_config = AppConfig { - client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - tenant_id: None, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri, - }; - Ok(AuthorizationCodeCertificateCredential { - app_config, + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + redirect_uri, + ), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, code_verifier: None, diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 7d9368ea..f242320a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -1,17 +1,21 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::IntoUrl; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; +use graph_extensions::crypto::ProofKeyCodeExchange; + use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; -use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; -use graph_extensions::crypto::ProofKeyCodeExchange; -use http::{HeaderMap, HeaderName, HeaderValue}; -use reqwest::IntoUrl; -use std::collections::HashMap; -use url::Url; -use uuid::Uuid; credential_builder!( AuthorizationCodeCredentialBuilder, @@ -58,6 +62,15 @@ pub struct AuthorizationCodeCredential { serializer: OAuthSerializer, } +impl Debug for AuthorizationCodeCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthorizationCodeCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} + impl AuthorizationCodeCredential { pub fn new<T: AsRef<str>, U: IntoUrl>( tenant_id: T, @@ -86,18 +99,12 @@ impl AuthorizationCodeCredential { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; - let app_config = AppConfig { - tenant_id: Some(tenant_id.as_ref().to_owned()), - client_id: Uuid::try_parse(client_id.as_ref())?, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri), - }; - Ok(AuthorizationCodeCredential { - app_config, + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Some(tenant_id.as_ref().to_owned()), + Some(redirect_uri), + ), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 97e7e049..cff94270 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -1,14 +1,18 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue}; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; + use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; -use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; -use http::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; -use url::Url; -use uuid::Uuid; credential_builder!( ClientAssertionCredentialBuilder, @@ -46,6 +50,16 @@ impl ClientAssertionCredential { } } +impl Debug for ClientAssertionCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientAssertionCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} + +#[derive(Clone)] pub struct ClientAssertionCredentialBuilder { credential: ClientAssertionCredential, } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 79996bdc..18f524f2 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -1,15 +1,18 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + use async_trait::async_trait; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; use url::Url; use uuid::Uuid; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; use crate::oauth::{ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication}; pub(crate) static CLIENT_ASSERTION_TYPE: &str = @@ -74,6 +77,14 @@ impl ClientCertificateCredential { } } +impl Debug for ClientCertificateCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientAssertionCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { fn uri(&mut self) -> IdentityResult<Url> { @@ -166,6 +177,7 @@ impl TokenCredentialExecutor for ClientCertificateCredential { } } +#[derive(Clone)] pub struct ClientCertificateCredentialBuilder { credential: ClientCertificateCredential, } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 1e03c1e1..3d8a613b 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,12 +1,14 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance}; -use graph_error::{AuthorizationFailure, IdentityResult}; use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; use uuid::Uuid; +use graph_error::{AuthorizationFailure, IdentityResult}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{Authority, AzureCloudInstance}; + #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrl { /// The client (application) ID of the service principal @@ -23,15 +25,11 @@ impl ClientCredentialsAuthorizationUrl { let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; Ok(ClientCredentialsAuthorizationUrl { - app_config: AppConfig { - tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref())?, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri), - }, + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + Some(redirect_uri), + ), state: None, }) } @@ -93,6 +91,7 @@ impl ClientCredentialsAuthorizationUrl { } } +#[derive(Clone)] pub struct ClientCredentialsAuthorizationUrlBuilder { parameters: ClientCredentialsAuthorizationUrl, } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 50a6cf52..c93d2184 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -1,17 +1,22 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{ - Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, - ConfidentialClientApplication, TokenCredentialExecutor, -}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; use async_trait::async_trait; -use graph_error::{AuthorizationFailure, IdentityResult}; use http::{HeaderMap, HeaderName, HeaderValue}; -use std::collections::HashMap; use url::Url; use uuid::Uuid; +use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; +use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::token::MsalToken; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ConfidentialClient, + ConfidentialClientApplication, TokenCredentialExecutor, +}; + credential_builder!(ClientSecretCredentialBuilder, ConfidentialClientApplication); /// Client Credentials flow using a client secret. @@ -24,7 +29,7 @@ credential_builder!(ClientSecretCredentialBuilder, ConfidentialClientApplication /// without immediate interaction with a user, and is often referred to as daemons or service accounts. /// /// See [Microsoft identity platform and the OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ClientSecretCredential { pub(crate) app_config: AppConfig, /// Required @@ -43,6 +48,16 @@ pub struct ClientSecretCredential { /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, + token_cache: InMemoryCredentialStore<MsalToken>, +} + +impl Debug for ClientSecretCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientSecretCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } } impl ClientSecretCredential { @@ -52,6 +67,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), + token_cache: InMemoryCredentialStore::new(), } } @@ -65,6 +81,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), + token_cache: InMemoryCredentialStore::new(), } } @@ -75,6 +92,49 @@ impl ClientSecretCredential { } } +#[async_trait] +impl TokenCacheStore for ClientSecretCredential { + type Token = MsalToken; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired() { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired() { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { fn uri(&mut self) -> IdentityResult<Url> { @@ -144,6 +204,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { } } +#[derive(Clone)] pub struct ClientSecretCredentialBuilder { credential: ClientSecretCredential, } @@ -165,6 +226,7 @@ impl ClientSecretCredentialBuilder { client_secret: client_secret.as_ref().to_string(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: Default::default(), + token_cache: InMemoryCredentialStore::new(), }, } } @@ -174,6 +236,10 @@ impl ClientSecretCredentialBuilder { self } + pub fn build_client(&self) -> ConfidentialClient<ClientSecretCredential> { + ConfidentialClient::new(self.credential.clone()) + } + pub fn credential(&self) -> ClientSecretCredential { self.credential.clone() } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index c281dbfb..aec4abf3 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,26 +1,140 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use dyn_clone::DynClone; +use reqwest::tls::Version; +use reqwest::{ClientBuilder, Response}; +use url::Url; +use uuid::Uuid; + +use graph_error::{AuthExecutionResult, IdentityResult}; +use graph_extensions::cache::{AsBearer, AutomaticTokenRefresh, TokenCacheStore, TokenStore}; +use graph_extensions::token::ClientApplication; + use crate::identity::credentials::app_config::AppConfig; use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; use crate::identity::credentials::client_assertion_credential::ClientAssertionCredential; use crate::identity::{ Authority, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, - TokenCredentialExecutor, UnInitializedCredentialExecutor, + TokenCredentialExecutor, }; -use async_trait::async_trait; -use graph_error::{AuthExecutionResult, IdentityResult, AF}; +pub struct ClientCache {} -use crate::oauth::MsalToken; -use graph_extensions::cache::{ - AutomaticTokenRefresh, InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, - UnInitializedTokenStore, -}; -use graph_extensions::token::ClientApplication; -use reqwest::tls::Version; -use reqwest::{ClientBuilder, Response}; -use std::collections::HashMap; -use url::Url; -use uuid::Uuid; +#[derive(Clone, Debug)] +pub struct ConfidentialClient<Credential> { + credential: Credential, +} + +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> ConfidentialClient<Credential> { + pub fn new(credential: Credential) -> ConfidentialClient<Credential> { + ConfidentialClient { credential } + } + + pub fn credential(credential: Credential) -> ConfidentialClient<Credential> { + ConfidentialClient { credential } + } + + pub fn builder(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { + ConfidentialClientApplicationBuilder::new(client_id) + } +} + +#[async_trait] +impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication + for ConfidentialClient<Credential> +{ + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + let token = self.credential.get_token_silent()?; + Ok(token.as_bearer()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + let token = self.credential.get_token_silent_async().await?; + Ok(token.as_bearer()) + } +} + +#[async_trait] +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor + for ConfidentialClient<Credential> +{ + fn uri(&mut self) -> IdentityResult<Url> { + self.credential.uri() + } + + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + self.credential.form_urlencode() + } + + fn client_id(&self) -> &Uuid { + self.credential.client_id() + } + + fn authority(&self) -> Authority { + self.credential.authority() + } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.credential.azure_cloud_instance() + } + + fn basic_auth(&self) -> Option<(String, String)> { + self.credential.basic_auth() + } + + fn app_config(&self) -> &AppConfig { + self.credential.app_config() + } + + fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { + self.credential.execute() + } + + async fn execute_async(&mut self) -> AuthExecutionResult<Response> { + self.credential.execute_async().await + } +} + +impl From<AuthorizationCodeCredential> for ConfidentialClient<AuthorizationCodeCredential> { + fn from(value: AuthorizationCodeCredential) -> Self { + ConfidentialClient::new(value) + } +} + +impl From<AuthorizationCodeCertificateCredential> + for ConfidentialClient<AuthorizationCodeCertificateCredential> +{ + fn from(value: AuthorizationCodeCertificateCredential) -> Self { + ConfidentialClient::credential(value) + } +} + +impl From<ClientSecretCredential> for ConfidentialClient<ClientSecretCredential> { + fn from(value: ClientSecretCredential) -> Self { + ConfidentialClient::credential(value) + } +} + +impl From<ClientCertificateCredential> for ConfidentialClient<ClientCertificateCredential> { + fn from(value: ClientCertificateCredential) -> Self { + ConfidentialClient::credential(value) + } +} + +impl From<ClientAssertionCredential> for ConfidentialClient<ClientAssertionCredential> { + fn from(value: ClientAssertionCredential) -> Self { + ConfidentialClient::credential(value) + } +} + +impl From<OpenIdCredential> for ConfidentialClient<OpenIdCredential> { + fn from(value: OpenIdCredential) -> Self { + ConfidentialClient::credential(value) + } +} /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), @@ -49,9 +163,14 @@ use uuid::Uuid; pub struct ConfidentialClientApplication { http_client: reqwest::Client, credential: Box<dyn TokenCredentialExecutor + Send>, - token_store: Box<dyn TokenStore + Send>, - token_watch: AutomaticTokenRefresh<String>, - token_sender: tokio::sync::watch::Sender<String>, +} + +impl Debug for ConfidentialClientApplication { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfidentialClientApplication") + .field("credential", &self.credential) + .finish() + } } impl ConfidentialClientApplication { @@ -75,9 +194,6 @@ impl ConfidentialClientApplication { .build() .unwrap(), credential: Box::new(credential), - token_store: Box::new(UnInitializedTokenStore), - token_watch, - token_sender, } } @@ -85,22 +201,6 @@ impl ConfidentialClientApplication { ConfidentialClientApplicationBuilder::new(client_id) } - pub fn init_automatic_refresh_token(&mut self) { - let rx = self.token_sender.subscribe(); - tokio::spawn(async move { - while rx.changed().await.is_ok() { - println!("received = {:?}", *rx.borrow()); - } - }); - } - - pub fn with_in_memory_token_store(&mut self) { - self.token_store = Box::new(InMemoryCredentialStore::new( - self.app_config().cache_id(), - StoredToken::UnInitialized, - )); - } - /* fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let response = self.get_openid_config()?; @@ -133,6 +233,7 @@ impl ConfidentialClientApplication { */ } +/* #[async_trait] impl ClientApplication for ConfidentialClientApplication { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { @@ -196,7 +297,9 @@ impl ClientApplication for ConfidentialClientApplication { self.token_store.get_stored_token(cache_id.as_str()) } } + */ +/* impl TokenStore for ConfidentialClientApplication { fn token_store_provider(&self) -> TokenStoreProvider { self.token_store.token_store_provider() @@ -223,6 +326,7 @@ impl TokenStore for ConfidentialClientApplication { } } + */ #[async_trait] impl TokenCredentialExecutor for ConfidentialClientApplication { fn uri(&mut self) -> IdentityResult<Url> { @@ -298,16 +402,11 @@ impl From<OpenIdCredential> for ConfidentialClientApplication { } } -impl From<UnInitializedCredentialExecutor> for ConfidentialClientApplication { - fn from(value: UnInitializedCredentialExecutor) -> Self { - ConfidentialClientApplication::credential(value) - } -} - #[cfg(test)] mod test { + use crate::identity::Authority; + use super::*; - use crate::identity::{Authority, AzureCloudInstance}; #[test] fn confidential_client_new() { @@ -374,73 +473,75 @@ mod test { ); } - #[test] - fn in_memory_token_store_init() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - client_id_string, - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent().unwrap(), - "token".to_string() - ) - } - - #[tokio::test] - async fn in_memory_token_store_init_async() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - client_id_string, - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent_async().await.unwrap(), - "token".to_string() - ) - } - - #[tokio::test] - async fn in_memory_token_store_tenant_and_client_cache_id() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_tenant("tenant-id") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - format!("{},{}", "tenant-id", client_id_string), - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent_async().await.unwrap(), - "token".to_string() - ) - } + /* + #[test] + fn in_memory_token_store_init() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + client_id_string, + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent().unwrap(), + "token".to_string() + ) + } + + #[tokio::test] + async fn in_memory_token_store_init_async() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + client_id_string, + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent_async().await.unwrap(), + "token".to_string() + ) + } + + #[tokio::test] + async fn in_memory_token_store_tenant_and_client_cache_id() { + let client_id = Uuid::new_v4(); + let client_id_string = client_id.to_string(); + let mut confidential_client = + ConfidentialClientApplication::builder(client_id_string.as_str()) + .with_authorization_code("code") + .with_tenant("tenant-id") + .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") + .with_scope(vec!["Read.Write", "Fall.Down"]) + .with_redirect_uri("http://localhost:8888/redirect") + .unwrap() + .build(); + + confidential_client.token_store = Box::new(InMemoryCredentialStore::new( + format!("{},{}", "tenant-id", client_id_string), + StoredToken::BearerToken("token".into()), + )); + assert_eq!( + confidential_client.get_token_silent_async().await.unwrap(), + "token".to_string() + ) + } + */ } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 3b47dd45..bbecb2b3 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,24 +1,25 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; -use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; - -use graph_error::{ - AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, - IdentityResult, AF, -}; -use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; use std::ops::Add; use std::str::FromStr; use std::time::Duration; -use crate::identity::credentials::app_config::AppConfig; +use http::{HeaderMap, HeaderName, HeaderValue}; +use url::Url; +use uuid::Uuid; +use graph_error::{ + AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, + IdentityResult, AF, +}; use graph_extensions::http::{ AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, }; -use url::Url; -use uuid::Uuid; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; +use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -82,6 +83,14 @@ impl DeviceCodeCredential { } } +impl Debug for DeviceCodeCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DeviceCodeCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} impl TokenCredentialExecutor for DeviceCodeCredential { fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index b2feb293..455fc1d8 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -1,16 +1,19 @@ +use std::collections::HashMap; +use std::env::VarError; +use std::fmt::{Debug, Formatter}; + +use url::Url; +use uuid::Uuid; + +use graph_error::IdentityResult; + use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationSerializer, AzureCloudInstance, ClientSecretCredential, - TokenCredentialExecutor, + Authority, AzureCloudInstance, ClientSecretCredential, TokenCredentialExecutor, }; use crate::oauth::{ ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, }; -use graph_error::IdentityResult; -use std::collections::HashMap; -use std::env::VarError; -use url::Url; -use uuid::Uuid; const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; const AZURE_CLIENT_ID: &str = "AZURE_CLIENT_ID"; @@ -23,6 +26,12 @@ pub struct EnvironmentCredential { pub credential: Box<dyn TokenCredentialExecutor + Send>, } +impl Debug for EnvironmentCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EnvironmentCredential").finish() + } +} + impl EnvironmentCredential { pub fn resource_owner_password_credential() -> Result<PublicClientApplication, VarError> { match EnvironmentCredential::try_username_password_compile_time_env() { @@ -164,6 +173,7 @@ impl TokenCredentialExecutor for EnvironmentCredential { } } +/* impl From<ClientSecretCredential> for EnvironmentCredential { fn from(value: ClientSecretCredential) -> Self { EnvironmentCredential { @@ -195,3 +205,5 @@ impl From<PublicClientApplication> for EnvironmentCredential { } } } + + */ diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index 46532740..e3351b58 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -1,9 +1,11 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::ResponseType; -use graph_error::{AuthorizationFailure, IdentityResult}; use url::form_urlencoded::Serializer; use url::Url; +use graph_error::{AuthorizationFailure, IdentityResult}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::oauth::ResponseType; + /// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive /// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. /// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs index 4aa4f251..322bcfca 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs @@ -1,9 +1,12 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AuthorizationSerializer, AzureCloudInstance}; -use graph_error::{AuthorizationFailure, IdentityResult}; use std::collections::HashMap; + use url::Url; +use graph_error::{AuthorizationFailure, IdentityResult}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::{Authority, AuthorizationSerializer, AzureCloudInstance}; + /// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive /// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. /// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 0d98e6ac..d62587fa 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,9 +1,5 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; - use graph_error::{AuthorizationFailure, IdentityResult}; -use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; +use graph_extensions::crypto::{secure_random_32, GenPkce}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -11,6 +7,10 @@ use url::form_urlencoded::Serializer; use url::Url; use uuid::*; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; + credential_builder_base!(ImplicitCredentialBuilder); /// The defining characteristic of the implicit grant is that tokens (ID tokens or access tokens) diff --git a/graph-oauth/src/identity/credentials/legacy/mod.rs b/graph-oauth/src/identity/credentials/legacy/mod.rs index f2d527c5..631ba03d 100644 --- a/graph-oauth/src/identity/credentials/legacy/mod.rs +++ b/graph-oauth/src/identity/credentials/legacy/mod.rs @@ -1,7 +1,9 @@ mod code_flow_authorization_url; mod code_flow_credential; +mod implicit_credential; mod token_flow_authorization_url; pub use code_flow_authorization_url::*; pub use code_flow_credential::*; +pub use implicit_credential::*; pub use token_flow_authorization_url::*; diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index 4557ff5e..fc74d03f 100644 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -1,9 +1,11 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::ResponseType; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; use url::form_urlencoded::Serializer; use url::Url; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::oauth::ResponseType; + /// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive /// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. /// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#token-flow diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 40d6dcf0..e4e45262 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,3 +1,29 @@ +pub use application_builder::*; +pub use as_query::*; +pub use auth_code_authorization_url::*; +pub use authorization_code_certificate_credential::*; +pub use authorization_code_credential::*; +pub use client_builder_impl::*; +pub use client_certificate_credential::*; +pub use client_credentials_authorization_url::*; +pub use client_secret_credential::*; +pub use confidential_client_application::*; +pub use device_code_credential::*; +pub use display::*; +pub use environment_credential::*; +pub use open_id_authorization_url::*; +pub use open_id_credential::*; +pub use prompt::*; +pub use public_client_application::*; +pub use resource_owner_password_credential::*; +pub use response_mode::*; +pub use response_type::*; +pub use token_credential_executor::*; +pub use token_credential_options::*; +pub use token_request::*; +#[cfg(feature = "openssl")] +pub use x509_certificate::*; + #[macro_use] mod client_builder_impl; @@ -17,7 +43,6 @@ mod confidential_client_application; mod device_code_credential; mod display; mod environment_credential; -mod implicit_credential; mod open_id_authorization_url; mod open_id_credential; mod prompt; @@ -31,31 +56,3 @@ mod token_request; #[cfg(feature = "openssl")] mod x509_certificate; - -pub use application_builder::*; -pub use as_query::*; -pub use auth_code_authorization_url::*; -pub use authorization_code_certificate_credential::*; -pub use authorization_code_credential::*; -pub use client_builder_impl::*; -pub use client_certificate_credential::*; -pub use client_credentials_authorization_url::*; -pub use client_secret_credential::*; -pub use confidential_client_application::*; -pub use device_code_credential::*; -pub use display::*; -pub use environment_credential::*; -pub use implicit_credential::*; -pub use open_id_authorization_url::*; -pub use open_id_credential::*; -pub use prompt::*; -pub use public_client_application::*; -pub use resource_owner_password_credential::*; -pub use response_mode::*; -pub use response_type::*; -pub use token_credential_executor::*; -pub use token_credential_options::*; -pub use token_request::*; - -#[cfg(feature = "openssl")] -pub use x509_certificate::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index fba21488..34304c12 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -1,27 +1,31 @@ +use std::collections::BTreeSet; +use std::fmt::{Debug, Formatter}; + +use reqwest::IntoUrl; +use url::form_urlencoded::Serializer; +use url::Url; +use uuid::Uuid; + +use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use graph_extensions::crypto::{secure_random_32, GenPkce}; + use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; -use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; -use reqwest::IntoUrl; -use std::collections::BTreeSet; -use url::form_urlencoded::Serializer; -use url::Url; -use uuid::Uuid; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your /// OAuth-enabled applications by using a security token called an ID token. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct OpenIdAuthorizationUrl { pub(crate) app_config: AppConfig, - /// Required + /// Required - /// Must include code for OpenID Connect sign-in. pub(crate) response_type: BTreeSet<ResponseType>, - /// Optional + /// Optional - /// Specifies how the identity platform should return the requested token to your app. /// /// Specifies the method that should be used to send the resulting authorization code back @@ -48,19 +52,19 @@ pub struct OpenIdAuthorizationUrl { /// is the same value it sent when requesting the token. The value is typically a unique, /// random string. pub(crate) nonce: String, - /// Required + /// Required - /// A value included in the request that also will be returned in the token response. /// It can be a string of any content you want. A randomly generated unique value typically /// is used to prevent cross-site request forgery attacks. The state also is used to encode /// information about the user's state in the app before the authentication request occurred, /// such as the page or view the user was on. pub(crate) state: Option<String>, - /// Required - the openid scope is already included + /// Required - the openid scope is already included. /// A space-separated list of scopes. For OpenID Connect, it must include the scope openid, /// which translates to the Sign you in permission in the consent UI. You might also include /// other scopes in this request for requesting consent. pub(crate) scope: BTreeSet<String>, - /// Optional + /// Optional - /// Indicates the type of user interaction that is required. The only valid values at /// this time are login, none, consent, and select_account. /// @@ -81,13 +85,13 @@ pub struct OpenIdAuthorizationUrl { /// allowing the user to pick which account they intend to sign in with, without requiring /// credential entry. You can't use both login_hint and select_account. pub(crate) prompt: BTreeSet<Prompt>, - /// Optional + /// Optional - /// The realm of the user in a federated directory. This skips the email-based discovery /// process that the user goes through on the sign-in page, for a slightly more streamlined /// user experience. For tenants that are federated through an on-premises directory /// like AD FS, this often results in a seamless sign-in because of the existing login session. pub(crate) domain_hint: Option<String>, - /// Optional + /// Optional - /// You can use this parameter to pre-fill the username and email address field of the /// sign-in page for the user, if you know the username ahead of time. Often, apps use /// this parameter during re-authentication, after already extracting the login_hint @@ -96,39 +100,41 @@ pub struct OpenIdAuthorizationUrl { response_types_supported: Vec<String>, } +impl Debug for OpenIdAuthorizationUrl { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthCodeAuthorizationUrlParameters") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .field("response_type", &self.response_type) + .field("response_mode", &self.response_mode) + .field("prompt", &self.prompt) + .finish() + } +} impl OpenIdAuthorizationUrl { pub fn new<T: AsRef<str>, IU: IntoUrl, U: ToString, I: IntoIterator<Item = U>>( client_id: T, redirect_uri: IU, scope: I, ) -> IdentityResult<OpenIdAuthorizationUrl> { - let mut scope_set = BTreeSet::new(); - scope_set.insert("openid".to_owned()); - scope_set.extend(scope.into_iter().map(|s| s.to_string())); - let redirect_uri_result = Url::parse(redirect_uri.as_str()); - - /* - AppConfig { - tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref())?, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - } - */ + let mut tree_set_scope = BTreeSet::new(); + tree_set_scope.insert("openid".to_owned()); + tree_set_scope.extend(scope.into_iter().map(|s| s.to_string())); + let redirect_uri_result = Url::parse(redirect_uri.as_str()); let mut app_config = AppConfig::new_with_client_id(client_id); app_config.redirect_uri = Some(redirect_uri.into_url().or(redirect_uri_result)?); + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::IdToken); + Ok(OpenIdAuthorizationUrl { app_config, - response_type: BTreeSet::new(), + response_type, response_mode: None, nonce: secure_random_32()?, state: None, - scope: scope_set, + scope: tree_set_scope, prompt: BTreeSet::new(), domain_hint: None, login_hint: None, @@ -141,12 +147,38 @@ impl OpenIdAuthorizationUrl { }) } + fn new_with_app_config(app_config: AppConfig) -> IdentityResult<OpenIdAuthorizationUrl> { + let mut scope = BTreeSet::new(); + scope.insert("openid".to_owned()); + + let mut response_type = BTreeSet::new(); + response_type.insert(ResponseType::IdToken); + + Ok(OpenIdAuthorizationUrl { + app_config, + response_type, + response_mode: None, + nonce: secure_random_32()?, + state: None, + scope, + prompt: Default::default(), + domain_hint: None, + login_hint: None, + response_types_supported: vec![ + "code".into(), + "id_token".into(), + "code id_token".into(), + "id_token token".into(), + ], + }) + } + pub fn builder(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { OpenIdAuthorizationUrlBuilder::new(client_id) } pub fn url(&self) -> IdentityResult<Url> { - self.url_with_host(&AzureCloudInstance::default()) + self.authorization_url() } pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { @@ -170,7 +202,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } fn authorization_url(&self) -> IdentityResult<Url> { - self.authorization_url_with_host(&AzureCloudInstance::default()) + self.authorization_url_with_host(&self.app_config.azure_cloud_instance) } fn authorization_url_with_host( @@ -275,40 +307,15 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } pub struct OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl, + parameters: OpenIdAuthorizationUrl, } impl OpenIdAuthorizationUrlBuilder { pub(crate) fn new(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { - let mut scope = BTreeSet::new(); - scope.insert("openid".to_owned()); - Ok(OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl { - app_config: AppConfig { - tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref())?, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: None, - }, - response_type: BTreeSet::new(), - response_mode: None, - nonce: secure_random_32()?, - state: None, - scope, - prompt: Default::default(), - domain_hint: None, - login_hint: None, - response_types_supported: vec![ - "code".into(), - "id_token".into(), - "code id_token".into(), - "id_token token".into(), - ], - }, + parameters: OpenIdAuthorizationUrl::new_with_app_config( + AppConfig::new_with_client_id(client_id), + )?, }) } @@ -316,32 +323,9 @@ impl OpenIdAuthorizationUrlBuilder { let mut scope = BTreeSet::new(); scope.insert("openid".to_owned()); - let nonce = match ProofKeyCodeExchange::code_verifier() { - Ok(secure_string) => secure_string, - Err(err) => { - error!("OpenIdAuthorizationUrlBuilder nonce: Crypto::sha256_secure_string() - internal error please report"); - panic!("{}", err); - } - }; - OpenIdAuthorizationUrlBuilder { - auth_url_parameters: OpenIdAuthorizationUrl { - app_config, - response_type: BTreeSet::new(), - response_mode: None, - nonce, - state: None, - scope, - prompt: Default::default(), - domain_hint: None, - login_hint: None, - response_types_supported: vec![ - "code".into(), - "id_token".into(), - "code id_token".into(), - "id_token token".into(), - ], - }, + parameters: OpenIdAuthorizationUrl::new_with_app_config(app_config) + .expect("ring::crypto::Unspecified"), } } @@ -349,25 +333,24 @@ impl OpenIdAuthorizationUrlBuilder { &mut self, redirect_uri: T, ) -> IdentityResult<&mut Self> { - self.auth_url_parameters.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); + self.parameters.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); Ok(self) } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.auth_url_parameters.app_config.client_id = + self.parameters.app_config.client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.auth_url_parameters.app_config.authority = - Authority::TenantId(tenant.as_ref().to_owned()); + self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.auth_url_parameters.app_config.authority = authority.into(); + self.parameters.app_config.authority = authority.into(); self } @@ -385,7 +368,7 @@ impl OpenIdAuthorizationUrlBuilder { &mut self, response_type: I, ) -> &mut Self { - self.auth_url_parameters.response_type = BTreeSet::from_iter(response_type.into_iter()); + self.parameters.response_type = BTreeSet::from_iter(response_type.into_iter()); self } @@ -398,7 +381,7 @@ impl OpenIdAuthorizationUrlBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.auth_url_parameters.response_mode = Some(response_mode); + self.parameters.response_mode = Some(response_mode); self } @@ -413,10 +396,10 @@ impl OpenIdAuthorizationUrlBuilder { /// authorization code grant. If you are unsure or unclear how the nonce works then it is /// recommended to stay with the generated nonce as it is cryptographically secure. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - if self.auth_url_parameters.nonce.is_empty() { - self.auth_url_parameters.nonce.push_str(nonce.as_ref()); + if self.parameters.nonce.is_empty() { + self.parameters.nonce.push_str(nonce.as_ref()); } else { - self.auth_url_parameters.nonce = nonce.as_ref().to_owned(); + self.parameters.nonce = nonce.as_ref().to_owned(); } self } @@ -434,22 +417,34 @@ impl OpenIdAuthorizationUrlBuilder { /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. #[doc(hidden)] pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.auth_url_parameters.nonce = secure_random_32()?; + self.parameters.nonce = secure_random_32()?; Ok(self) } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.auth_url_parameters.state = Some(state.as_ref().to_owned()); + self.parameters.state = Some(state.as_ref().to_owned()); self } /// Takes an iterator of scopes to use in the request. /// Replaces current scopes if any were added previously. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.auth_url_parameters.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.parameters.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } + /// Automatically adds `profile` and `email` to the scope parameter. + /// The `openid` scope is already included in the request. + /// + /// If you need a refresh token then include `offline_access` as a scope. + /// The `offline_access` scope is not included here. + pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { + self.parameters + .scope + .extend(vec!["profile".to_owned(), "email".to_owned()]); + Ok(self) + } + /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// @@ -461,7 +456,7 @@ impl OpenIdAuthorizationUrlBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self { - self.auth_url_parameters.prompt.extend(prompt.into_iter()); + self.parameters.prompt.extend(prompt.into_iter()); self } @@ -471,7 +466,7 @@ impl OpenIdAuthorizationUrlBuilder { /// user experience. For tenants that are federated through an on-premises directory /// like AD FS, this often results in a seamless sign-in because of the existing login session. pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.auth_url_parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); self } @@ -481,20 +476,16 @@ impl OpenIdAuthorizationUrlBuilder { /// this parameter during reauthentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.auth_url_parameters.login_hint = Some(login_hint.as_ref().to_owned()); + self.parameters.login_hint = Some(login_hint.as_ref().to_owned()); self } pub fn build(&self) -> OpenIdAuthorizationUrl { - self.auth_url_parameters.clone() - } - - pub fn nonce(&self) -> &String { - &self.auth_url_parameters.nonce + self.parameters.clone() } pub fn url(&self) -> IdentityResult<Url> { - self.auth_url_parameters.url() + self.parameters.url() } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index dd4cc15b..418267b9 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -1,17 +1,21 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::IntoUrl; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; +use graph_extensions::crypto::{GenPkce, ProofKeyCodeExchange}; + use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, OpenIdAuthorizationUrl, TokenCredentialExecutor, }; use crate::oauth::{ConfidentialClientApplication, OpenIdAuthorizationUrlBuilder}; -use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; -use graph_extensions::crypto::{GenPkce, ProofKeyCodeExchange}; -use http::{HeaderMap, HeaderName, HeaderValue}; -use reqwest::IntoUrl; -use std::collections::HashMap; -use url::Url; -use uuid::Uuid; credential_builder!(OpenIdCredentialBuilder, ConfidentialClientApplication); @@ -59,6 +63,15 @@ pub struct OpenIdCredential { serializer: OAuthSerializer, } +impl Debug for OpenIdCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpenIdCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} + impl OpenIdCredential { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, @@ -68,15 +81,11 @@ impl OpenIdCredential { ) -> IdentityResult<OpenIdCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); Ok(OpenIdCredential { - app_config: AppConfig { - tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - }, + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + Some(redirect_uri.into_url().or(redirect_uri_result)?), + ), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), @@ -112,19 +121,11 @@ impl TokenCredentialExecutor for OpenIdCredential { self.serializer .authority(&azure_cloud_instance, &self.app_config.authority); - if self.refresh_token.is_none() { - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("access_token_url"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } else { - let uri = self - .serializer - .get(OAuthParameter::RefreshTokenUrl) - .ok_or(AF::msg_internal_err("refresh_token_url"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } + let uri = self + .serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_internal_err("access_token_url"))?; + Url::parse(uri.as_str()).map_err(AF::from) } fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { @@ -297,12 +298,13 @@ impl OpenIdCredentialBuilder { self } - pub fn with_pkce(&mut self, proof_key_for_code_exchange: &ProofKeyCodeExchange) -> &mut Self { - self.with_code_verifier(proof_key_for_code_exchange.code_verifier.as_str()); + pub fn with_pkce(&mut self, pkce: ProofKeyCodeExchange) -> &mut Self { + self.with_code_verifier(pkce.code_verifier.as_str()); + self.credential.pkce = Some(pkce); self } - pub fn generate_pkce(&mut self) -> IdentityResult<&mut Self> { + pub fn with_pkce_oneshot(&mut self) -> IdentityResult<&mut Self> { let pkce = ProofKeyCodeExchange::oneshot()?; self.with_code_verifier(pkce.code_verifier.as_str()); self.credential.pkce = Some(pkce); diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index 67ef34a0..90967ff1 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -1,6 +1,7 @@ -use crate::identity::credentials::as_query::AsQuery; use std::collections::BTreeSet; +use crate::identity::credentials::as_query::AsQuery; + /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 3d0d2022..5326733e 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,23 +1,23 @@ -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::credentials::application_builder::PublicClientApplicationBuilder; -use crate::identity::{ - Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, - TokenCredentialExecutor, -}; -use crate::oauth::UnInitializedCredentialExecutor; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + use async_trait::async_trait; -use graph_error::{AuthExecutionResult, IdentityResult, AF}; -use graph_extensions::cache::{ - InMemoryCredentialStore, StoredToken, TokenStore, TokenStoreProvider, UnInitializedTokenStore, -}; -use graph_extensions::token::{ClientApplication, ClientApplicationType, MsalToken}; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::{ClientBuilder, Response}; -use std::collections::HashMap; use url::Url; use uuid::Uuid; +use graph_error::{AuthExecutionResult, IdentityResult}; +use graph_extensions::token::ClientApplication; + +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::application_builder::PublicClientApplicationBuilder; +use crate::identity::{ + Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, + TokenCredentialExecutor, +}; + /// Clients incapable of maintaining the confidentiality of their credentials /// (e.g., clients executing on the device used by the resource owner, such as an /// installed native application or a web browser-based application), and incapable of @@ -27,7 +27,15 @@ use uuid::Uuid; pub struct PublicClientApplication { http_client: reqwest::Client, credential: Box<dyn TokenCredentialExecutor + Send>, - token_store: Box<dyn TokenStore + Send>, + //token_store: Arc<RwLock<dyn TokenStore + Send>>, +} + +impl Debug for PublicClientApplication { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ConfidentialClientApplication") + .field("credential", &self.credential) + .finish() + } } impl PublicClientApplication { @@ -49,7 +57,7 @@ impl PublicClientApplication { .build() .unwrap(), credential: Box::new(credential), - token_store: Box::new(UnInitializedTokenStore), + //token_store: Arc::new(RwLock::new(UnInitializedTokenStore)), } } @@ -57,17 +65,28 @@ impl PublicClientApplication { PublicClientApplicationBuilder::new(client_id.as_ref()) } - pub fn with_in_memory_token_store(&mut self) { - self.token_store = Box::new(InMemoryCredentialStore::new( + /* + pub fn with_in_memory_token_store(&mut self) { + self.token_store = Arc::new(RwLock::new(InMemoryCredentialStore::new( self.app_config().cache_id(), StoredToken::UnInitialized, - )); + ))); } + */ } #[async_trait] impl ClientApplication for PublicClientApplication { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + todo!() + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + todo!() + } +} +/* +fn get_token_silent(&mut self) -> AuthExecutionResult<String> { let cache_id = self.app_config().cache_id(); if self.is_store_and_token_initialized(cache_id.as_str()) { return Ok(self @@ -127,34 +146,35 @@ impl ClientApplication for PublicClientApplication { self.token_store.get_stored_token(cache_id.as_str()) } -} + */ +/* impl TokenStore for PublicClientApplication { fn token_store_provider(&self) -> TokenStoreProvider { - self.token_store.token_store_provider() + self.token_store.read().unwrap().token_store_provider() } fn is_stored_token_initialized(&self, id: &str) -> bool { - self.token_store.is_stored_token_initialized(id) + self.token_store.read().unwrap().is_stored_token_initialized(id) } fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { - self.token_store.get_stored_token(id) + self.token_store.read().unwrap().get_stored_token(id) } fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { - self.token_store.update_stored_token(id, stored_token) + *self.token_store.write().unwrap().update_stored_token(id, stored_token) } fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.get_bearer_token_from_store(id) + self.token_store.read().unwrap().get_bearer_token_from_store(id) } fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.get_refresh_token_from_store(id) + self.token_store.read().unwrap().get_refresh_token_from_store(id) } } - +*/ #[async_trait] impl TokenCredentialExecutor for PublicClientApplication { fn uri(&mut self) -> IdentityResult<Url> { @@ -252,9 +272,3 @@ impl From<DeviceCodeCredential> for PublicClientApplication { PublicClientApplication::credential(value) } } - -impl From<UnInitializedCredentialExecutor> for PublicClientApplication { - fn from(value: UnInitializedCredentialExecutor) -> Self { - PublicClientApplication::credential(value) - } -} diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index b5a5342a..b754434d 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,12 +1,16 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; -use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; use url::Url; use uuid::Uuid; +use graph_error::{IdentityResult, AF}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; + /// Allows an application to sign in the user by directly handling their password. /// Not recommended. ROPC can also be done using a client secret or assertion, /// however this client implementation does not offer this use case. This is the @@ -29,6 +33,15 @@ pub struct ResourceOwnerPasswordCredential { serializer: OAuthSerializer, } +impl Debug for ResourceOwnerPasswordCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientAssertionCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} + impl ResourceOwnerPasswordCredential { pub fn new<T: AsRef<str>>( client_id: T, @@ -52,8 +65,10 @@ impl ResourceOwnerPasswordCredential { username: T, password: T, ) -> ResourceOwnerPasswordCredential { + let mut app_config = AppConfig::new_with_tenant_and_client_id(tenant_id, client_id); + app_config.authority = Authority::Organizations; ResourceOwnerPasswordCredential { - app_config: AppConfig::new_with_tenant_and_client_id(tenant_id.as_ref(), client_id), + app_config, username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), scope: vec![], diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index 0c79e6dd..6a20bb40 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -1,6 +1,7 @@ -use crate::identity::AsQuery; use std::collections::BTreeSet; +use crate::identity::AsQuery; + #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum ResponseType { #[default] diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 0253dbe3..028c4b1c 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -1,25 +1,25 @@ -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance}; +use std::collections::HashMap; +use std::fmt::Debug; use async_trait::async_trait; use dyn_clone::DynClone; -use graph_error::{AuthExecutionResult, IdentityResult}; use http::header::ACCEPT; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; -use std::collections::HashMap; +use tracing::{debug, Instrument}; use url::Url; use uuid::Uuid; +use graph_error::{AuthExecutionResult, IdentityResult}; + +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{Authority, AzureCloudInstance}; + dyn_clone::clone_trait_object!(TokenCredentialExecutor); #[async_trait] -pub trait TokenCredentialExecutor: DynClone { - fn is_initialized(&self) -> bool { - true - } - +pub trait TokenCredentialExecutor: DynClone + Debug { fn uri(&mut self) -> IdentityResult<Url>; fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; @@ -144,6 +144,7 @@ pub trait TokenCredentialExecutor: DynClone { } } + #[tracing::instrument] async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { let mut uri = self.uri()?; let form = self.form_urlencode()?; @@ -178,50 +179,39 @@ pub trait TokenCredentialExecutor: DynClone { let basic_auth = self.basic_auth(); if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client + let request_builder = http_client .post(uri) .basic_auth(client_identifier, Some(secret)) .headers(headers) - .form(&form) - .send() - .await?) + .form(&form); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + let response = request_builder.send().await; + debug!("authorization response received; response={:#?}", response); + Ok(response?) } else { - Ok(http_client - .post(uri) - .headers(headers) - .form(&form) - .send() - .await?) + let request_builder = http_client.post(uri).headers(headers).form(&form); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + let response = request_builder.send().await; + debug!("authorization response received; response={:#?}", response); + Ok(response?) } } } -#[derive(Clone)] -pub struct UnInitializedCredentialExecutor; - -impl TokenCredentialExecutor for UnInitializedCredentialExecutor { - fn is_initialized(&self) -> bool { - false - } - - fn uri(&mut self) -> IdentityResult<Url> { - panic!("TokenCredentialExecutor is UnInitialized"); - } - - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - panic!("TokenCredentialExecutor is UnInitialized"); - } - - fn app_config(&self) -> &AppConfig { - panic!("TokenCredentialExecutor is UnInitialized"); - } -} - #[cfg(test)] mod test { - use super::*; use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; + use super::*; + #[test] fn open_id_configuration_url_authority_tenant_id() { let open_id = ConfidentialClientApplicationBuilder::new("client-id") diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs index 057c27a6..2eccbfb3 100644 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ b/graph-oauth/src/identity/credentials/token_request.rs @@ -1,11 +1,11 @@ -use crate::oauth::AuthorizationSerializer; use async_trait::async_trait; - -use crate::identity::AzureCloudInstance; use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; use reqwest::tls::Version; use reqwest::ClientBuilder; +use crate::identity::AzureCloudInstance; +use crate::oauth::AuthorizationSerializer; + #[async_trait] pub trait TokenRequest: AuthorizationSerializer { fn azure_cloud_instance(&self) -> AzureCloudInstance; diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 7ca4b882..7831a85a 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use anyhow::anyhow; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; @@ -8,7 +10,6 @@ use openssl::pkey::{PKey, Private}; use openssl::rsa::Padding; use openssl::sign::Signer; use openssl::x509::{X509Ref, X509}; -use std::collections::HashMap; use time::OffsetDateTime; use uuid::Uuid; diff --git a/graph-oauth/src/identity/device_code.rs b/graph-oauth/src/identity/device_code.rs index b777ee8e..6d7de299 100644 --- a/graph-oauth/src/identity/device_code.rs +++ b/graph-oauth/src/identity/device_code.rs @@ -1,7 +1,8 @@ -use serde_json::Value; use std::collections::{BTreeSet, HashMap}; use std::str::FromStr; +use serde_json::Value; + /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 /// The actual device code response that is received from Microsoft Graph /// looks similar to the following: diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 728fcecd..fc9ce972 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,23 +1,26 @@ -mod allowed_host_validator; -mod application_options; -mod authority; -mod authorization_query_response; -mod authorization_serializer; -mod credentials; -mod device_code; -mod token_validator; +#[cfg(feature = "openssl")] +pub use openssl::{ + pkey::{PKey, Private}, + x509::X509, +}; pub use allowed_host_validator::*; pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; pub use authorization_serializer::*; +pub use cache::*; pub use credentials::*; pub use device_code::*; pub use token_validator::*; -#[cfg(feature = "openssl")] -pub use openssl::{ - pkey::{PKey, Private}, - x509::X509, -}; +mod allowed_host_validator; +mod application_options; +mod authority; +mod authorization_query_response; +mod authorization_serializer; +mod cache; +mod credentials; +mod device_code; + +mod token_validator; diff --git a/graph-oauth/src/jwt.rs b/graph-oauth/src/jwt.rs index f9d147aa..e2603b44 100644 --- a/graph-oauth/src/jwt.rs +++ b/graph-oauth/src/jwt.rs @@ -1,12 +1,15 @@ -use crate::oauth_error::OAuthError; -use base64::Engine; -use graph_error::{GraphFailure, GraphResult}; -use serde_json::Map; -use serde_json::Value; use std::collections::HashMap; use std::convert::TryFrom; use std::str::FromStr; +use base64::Engine; +use serde_json::Map; +use serde_json::Value; + +use graph_error::{GraphFailure, GraphResult}; + +use crate::oauth_error::OAuthError; + /// Enum for the type of JSON web token (JWT). #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum JwtType { diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index fda286a7..6548b446 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -52,12 +52,12 @@ //! ``` #[macro_use] -extern crate strum; +extern crate log; +extern crate pretty_env_logger; #[macro_use] extern crate serde; #[macro_use] -extern crate log; -extern crate pretty_env_logger; +extern crate strum; mod auth; mod discovery; @@ -68,6 +68,10 @@ mod oauth_error; pub mod identity; pub mod oauth { + pub use graph_extensions::{ + crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, + }; + pub use crate::auth::GrantSelector; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; @@ -78,7 +82,4 @@ pub mod oauth { pub use crate::identity::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; - pub use graph_extensions::{ - crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, - }; } diff --git a/graph-oauth/src/oauth_error.rs b/graph-oauth/src/oauth_error.rs index 55414035..a35e9c7b 100644 --- a/graph-oauth/src/oauth_error.rs +++ b/graph-oauth/src/oauth_error.rs @@ -1,11 +1,13 @@ -use crate::auth::OAuthParameter; -use crate::grants::{GrantRequest, GrantType}; -use graph_error::{GraphFailure, GraphResult}; use std::error; use std::error::Error; use std::fmt; use std::io::ErrorKind; +use graph_error::{GraphFailure, GraphResult}; + +use crate::auth::OAuthParameter; +use crate::grants::{GrantRequest, GrantType}; + /// Error implementation for OAuth #[derive(Debug)] pub enum OAuthError { diff --git a/src/client/graph.rs b/src/client/graph.rs index c38cc4f4..8c0ef9c1 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -67,6 +67,7 @@ use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_error::GraphFailure; use graph_extensions::token::ClientApplication; use graph_http::api_impl::GraphClientConfiguration; +use graph_oauth::identity::{ClientSecretCredential, ConfidentialClient}; use lazy_static::lazy_static; use std::convert::TryFrom; @@ -562,6 +563,16 @@ impl From<GraphClientConfiguration> for Graph { } } +impl From<ConfidentialClient<ClientSecretCredential>> for Graph { + fn from(value: ConfidentialClient<ClientSecretCredential>) -> Self { + Graph { + client: Client::new(value), + endpoint: PARSED_GRAPH_URL.clone(), + allowed_host_validator: AllowedHostValidator::default(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index e8f4ab8a..41b32c42 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -12,7 +12,6 @@ use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; -use futures::TryFutureExt; use graph_http::api_impl::BearerToken; use std::sync::Mutex; From 9153ec183864a2d21488fda45e6eb73ec0cb81fc Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 18 Oct 2023 02:18:08 -0400 Subject: [PATCH 044/118] Update client applications to use generic over dynamic dispatch --- .../auth_code_grant/auth_code_grant_secret.rs | 20 +- .../legacy/implicit_grant.rs | 6 +- examples/users/todos/tasks.rs | 4 +- graph-error/src/authorization_failure.rs | 14 + graph-error/src/graph_failure.rs | 16 +- .../src/cache/in_memory_credential_store.rs | 7 +- graph-extensions/src/cache/token_store.rs | 7 +- .../src/cache/token_watch_task.rs | 2 +- .../src/token/client_application.rs | 1 - graph-extensions/src/token/msal_token.rs | 39 +- graph-http/src/blocking/blocking_client.rs | 3 +- .../src/blocking/blocking_request_handler.rs | 37 +- graph-http/src/client.rs | 66 +- graph-http/src/request_handler.rs | 2 +- graph-oauth/src/auth.rs | 1137 ----------------- .../src/identity/authorization_request.rs | 45 + .../identity/cache/in_memory_client_store.rs | 9 - graph-oauth/src/identity/cache/mod.rs | 3 - .../src/identity/credentials/app_config.rs | 23 +- .../credentials/application_builder.rs | 37 +- .../auth_code_authorization_url.rs | 60 +- ...authorization_code_assertion_credential.rs | 310 +++++ ...thorization_code_certificate_credential.rs | 55 +- .../authorization_code_credential.rs | 90 +- .../client_assertion_credential.rs | 56 +- .../credentials/client_builder_impl.rs | 8 + .../client_certificate_credential.rs | 60 +- .../credentials/client_secret_credential.rs | 25 +- .../confidential_client_application.rs | 419 +----- .../credentials/device_code_credential.rs | 15 +- .../credentials/environment_credential.rs | 27 +- .../credentials/force_token_refresh.rs | 14 + .../credentials/legacy/implicit_credential.rs | 4 +- graph-oauth/src/identity/credentials/mod.rs | 4 + .../credentials/open_id_authorization_url.rs | 25 +- .../credentials/open_id_credential.rs | 10 +- .../credentials/public_client_application.rs | 241 +--- .../credentials/token_credential_executor.rs | 95 +- .../identity/credentials/x509_certificate.rs | 114 +- graph-oauth/src/identity/mod.rs | 4 +- graph-oauth/src/lib.rs | 4 - src/client/graph.rs | 6 +- test-tools/src/oauth.rs | 81 -- test-tools/src/oauth_request.rs | 48 +- tests/grants_code_flow.rs | 133 -- tests/grants_implicit.rs | 19 - tests/grants_openid.rs | 49 - tests/grants_token_flow.rs | 20 - tests/mail_folder_request.rs | 1 - tests/token_cache_tests.rs | 35 + tests/upload_request_blocking.rs | 1 + 51 files changed, 1103 insertions(+), 2408 deletions(-) create mode 100644 graph-oauth/src/identity/authorization_request.rs delete mode 100644 graph-oauth/src/identity/cache/in_memory_client_store.rs delete mode 100644 graph-oauth/src/identity/cache/mod.rs create mode 100644 graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs create mode 100644 graph-oauth/src/identity/credentials/force_token_refresh.rs delete mode 100644 tests/grants_code_flow.rs delete mode 100644 tests/grants_implicit.rs delete mode 100644 tests/grants_openid.rs delete mode 100644 tests/grants_token_flow.rs create mode 100644 tests/token_cache_tests.rs diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index ffe71a28..24169ff1 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -56,18 +56,16 @@ pub async fn start_server_main() { /// # Use the access code to build Confidential Client Application /// /// ```rust -/// fn main() { -/// use graph_rs_sdk::oauth::ConfidentialClientApplication; +/// use graph_rs_sdk::oauth::ConfidentialClientApplication; /// -/// // Set the access code and request an access token. -/// // Callers should handle the Result from requesting an access token -/// // in case of an error here. -/// let client_app = ConfidentialClientApplication::builder("client-id") -/// .with_authorization_code("code") -/// .with_client_secret("client-secret") -/// .with_scope(vec!["User.Read"]) -/// .build(); -/// } +/// // Set the access code and request an access token. +/// // Callers should handle the Result from requesting an access token +/// // in case of an error here. +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); /// ``` async fn handle_redirect( code_option: Option<AccessCode>, diff --git a/examples/oauth_authorization_url/legacy/implicit_grant.rs b/examples/oauth_authorization_url/legacy/implicit_grant.rs index a50bcb90..897aa595 100644 --- a/examples/oauth_authorization_url/legacy/implicit_grant.rs +++ b/examples/oauth_authorization_url/legacy/implicit_grant.rs @@ -22,10 +22,8 @@ use std::collections::BTreeSet; // 2. Implicit grant flow for v2.0: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow // // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 -use graph_rs_sdk::oauth::{ - ImplicitCredential, Prompt, ResponseMode, ResponseType, TokenCredentialExecutor, -}; - +use graph_rs_sdk::oauth::legacy::ImplicitCredential; +use graph_rs_sdk::oauth::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; fn oauth_implicit_flow() { let authorizer = ImplicitCredential::builder() .with_client_id("<YOUR_CLIENT_ID>") diff --git a/examples/users/todos/tasks.rs b/examples/users/todos/tasks.rs index 2a564a91..7aa678e1 100644 --- a/examples/users/todos/tasks.rs +++ b/examples/users/todos/tasks.rs @@ -66,7 +66,7 @@ async fn create_task(user_id: &str, list_id: &str) -> GraphResult<()> { .todo() .list(list_id) .tasks() - .create_tasks(&task) + .create_tasks(task) .send() .await?; @@ -99,7 +99,7 @@ async fn create_task_using_me(list_id: &str) -> GraphResult<()> { .todo() .list(list_id) .tasks() - .create_tasks(&task) + .create_tasks(task) .send() .await?; diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 9f6a7909..f96a701f 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -19,6 +19,12 @@ pub enum AuthorizationFailure { #[error("{0:#?}")] Unknown(String), + + #[error("{0:#?}")] + X509Error(String), + + #[error("{0:#?}")] + SerdeJsonError(#[from] serde_json::Error), } impl AuthorizationFailure { @@ -83,6 +89,14 @@ impl AuthorizationFailure { Ok(()) } } + + pub fn x509(message: impl ToString) -> AuthorizationFailure { + AuthorizationFailure::X509Error(message.to_string()) + } + + pub fn x509_result<T>(message: impl ToString) -> Result<T, AuthorizationFailure> { + Err(AuthorizationFailure::X509Error(message.to_string())) + } } #[derive(Debug, thiserror::Error)] diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index 03203cc0..f79d5bfc 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -7,7 +7,6 @@ use std::io; use std::io::ErrorKind; use std::str::Utf8Error; use std::sync::mpsc; -use url::form_urlencoded::parse; #[derive(Debug, thiserror::Error)] #[allow(clippy::large_enum_variant)] @@ -114,8 +113,8 @@ impl From<ring::error::Unspecified> for GraphFailure { impl From<AuthExecutionError> for GraphFailure { fn from(value: AuthExecutionError) -> Self { match value { - AuthExecutionError::AuthorizationFailure(authorizationFailure) => { - match authorizationFailure { + AuthExecutionError::AuthorizationFailure(authorization_failure) => { + match authorization_failure { AuthorizationFailure::RequiredValue { name, message } => { GraphFailure::PreFlightError { url: None, @@ -125,7 +124,7 @@ impl From<AuthExecutionError> for GraphFailure { } } AuthorizationFailure::UrlParseError(e) => GraphFailure::UrlParseError(e), - AuthorizationFailure::UuidError(uuidError) => GraphFailure::PreFlightError { + AuthorizationFailure::UuidError(_uuid_error) => GraphFailure::PreFlightError { url: None, headers: None, error: None, @@ -137,6 +136,15 @@ impl From<AuthExecutionError> for GraphFailure { error: None, message, }, + AuthorizationFailure::X509Error(message) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message, + }, + AuthorizationFailure::SerdeJsonError(serde_json_error) => { + GraphFailure::SerdeError(serde_json_error) + } } } AuthExecutionError::RequestError(e) => GraphFailure::ReqwestError(e), diff --git a/graph-extensions/src/cache/in_memory_credential_store.rs b/graph-extensions/src/cache/in_memory_credential_store.rs index d50ba970..4237bfac 100644 --- a/graph-extensions/src/cache/in_memory_credential_store.rs +++ b/graph-extensions/src/cache/in_memory_credential_store.rs @@ -1,9 +1,8 @@ -use crate::cache::{AsBearer, StoredToken, TokenStore, TokenStoreProvider}; +use crate::cache::AsBearer; use std::collections::HashMap; -use std::hash::Hash; use std::sync::{Arc, RwLock}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct InMemoryCredentialStore<Token: AsBearer + Clone> { store: Arc<RwLock<HashMap<String, Token>>>, } @@ -21,7 +20,7 @@ impl<Token: AsBearer + Clone> InMemoryCredentialStore<Token> { } pub fn get(&self, cache_id: &str) -> Option<Token> { - let mut store = self.store.read().unwrap(); + let store = self.store.read().unwrap(); store.get(cache_id).cloned() } } diff --git a/graph-extensions/src/cache/token_store.rs b/graph-extensions/src/cache/token_store.rs index 934b058e..d810f09a 100644 --- a/graph-extensions/src/cache/token_store.rs +++ b/graph-extensions/src/cache/token_store.rs @@ -17,11 +17,8 @@ impl StoredToken { } pub fn enable_pii_logging(&mut self) { - match self { - StoredToken::MsalToken(token) => { - token.enable_pii_logging(true); - } - _ => {} + if let StoredToken::MsalToken(token) = self { + token.enable_pii_logging(true); } } diff --git a/graph-extensions/src/cache/token_watch_task.rs b/graph-extensions/src/cache/token_watch_task.rs index e72795d2..ef128939 100644 --- a/graph-extensions/src/cache/token_watch_task.rs +++ b/graph-extensions/src/cache/token_watch_task.rs @@ -8,7 +8,7 @@ pub struct AutomaticTokenRefresh<T> { impl<T: Clone + Debug + Send + Sync> AutomaticTokenRefresh<T> { pub fn new(init: T) -> (Sender<T>, AutomaticTokenRefresh<T>) { - let (tx, mut rx) = channel(init); + let (tx, rx) = channel(init); (tx, AutomaticTokenRefresh { rx }) } diff --git a/graph-extensions/src/token/client_application.rs b/graph-extensions/src/token/client_application.rs index fa90a324..4867f8d5 100644 --- a/graph-extensions/src/token/client_application.rs +++ b/graph-extensions/src/token/client_application.rs @@ -1,4 +1,3 @@ -use crate::cache::{StoredToken, TokenStore}; use async_trait::async_trait; use dyn_clone::DynClone; use graph_error::AuthExecutionResult; diff --git a/graph-extensions/src/token/msal_token.rs b/graph-extensions/src/token/msal_token.rs index f1a333b7..c50b0542 100644 --- a/graph-extensions/src/token/msal_token.rs +++ b/graph-extensions/src/token/msal_token.rs @@ -4,7 +4,7 @@ use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; -use std::ops::Add; +use std::ops::{Add, Sub}; use crate::token::IdToken; use std::str::FromStr; @@ -24,7 +24,7 @@ where // Used to set timestamp based on expires in // which can only be done after deserialization. -#[derive(Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct PhantomMsalToken { access_token: String, token_type: String, @@ -152,7 +152,7 @@ impl MsalToken { pub fn with_expires_in(&mut self, expires_in: i64) -> &mut Self { self.expires_in = expires_in; let timestamp = time::OffsetDateTime::now_utc(); - self.expires_on = Some(timestamp.add(time::Duration::seconds(self.expires_in.clone()))); + self.expires_on = Some(timestamp.add(time::Duration::seconds(self.expires_in))); self.timestamp = Some(timestamp); self } @@ -294,20 +294,16 @@ impl MsalToken { /// access_token.expires_in = 86999; /// access_token.gen_timestamp(); /// println!("{:#?}", access_token.timestamp); - /// // The timestamp is in UTC. /// ``` pub fn gen_timestamp(&mut self) { let timestamp = time::OffsetDateTime::now_utc(); - let expires_on = timestamp.add(time::Duration::seconds(self.expires_in.clone())); + let expires_on = timestamp.add(time::Duration::seconds(self.expires_in)); self.timestamp = Some(timestamp); self.expires_on = Some(expires_on); } - /// Check whether the access token is expired. Uses the expires_in - /// field to check time elapsed since token was first deserialized. - /// This is done using a Utc timestamp set when the [MsalToken] is - /// deserialized from the api response - /// + /// Check whether the access token is expired. Checks if expires_on timestamp + /// is less than UTC now timestamp. /// /// # Example /// ``` @@ -324,6 +320,25 @@ impl MsalToken { } } + /// Check whether the access token is expired sub duration. + /// This is useful in scenarios where you want to eagerly refresh + /// the access token before it expires to prevent a failed request. + /// + /// # Example + /// ``` + /// # use graph_extensions::token::MsalToken; + /// + /// let mut access_token = MsalToken::default(); + /// println!("{:#?}", access_token.is_expired_sub(time::Duration::minutes(5))); + /// ``` + pub fn is_expired_sub(&self, duration: time::Duration) -> bool { + if let Some(expires_on) = self.expires_on.as_ref() { + expires_on.sub(duration).lt(&OffsetDateTime::now_utc()) + } else { + false + } + } + /// Get the time left in seconds until the access token expires. /// See the HumanTime crate. If you just need to know if the access token /// is expired then use the is_expired() message which returns a boolean @@ -488,8 +503,8 @@ mod test { #[test] fn is_expired_test() { let mut access_token = MsalToken::default(); - access_token.with_expires_in(1); - std::thread::sleep(std::time::Duration::from_secs(3)); + access_token.with_expires_in(5); + std::thread::sleep(std::time::Duration::from_secs(6)); assert!(access_token.is_expired()); let mut access_token = MsalToken::default(); diff --git a/graph-http/src/blocking/blocking_client.rs b/graph-http/src/blocking/blocking_client.rs index d37f7ecb..5e828515 100644 --- a/graph-http/src/blocking/blocking_client.rs +++ b/graph-http/src/blocking/blocking_client.rs @@ -1,4 +1,5 @@ use crate::internal::GraphClientConfiguration; +use graph_extensions::token::ClientApplication; use reqwest::header::HeaderMap; use std::env::VarError; use std::ffi::OsStr; @@ -6,8 +7,8 @@ use std::fmt::{Debug, Formatter}; #[derive(Clone)] pub struct BlockingClient { - pub(crate) access_token: String, pub(crate) inner: reqwest::blocking::Client, + pub(crate) client_application: Box<dyn ClientApplication>, pub(crate) headers: HeaderMap, } diff --git a/graph-http/src/blocking/blocking_request_handler.rs b/graph-http/src/blocking/blocking_request_handler.rs index 134b99c2..df5acb0d 100644 --- a/graph-http/src/blocking/blocking_request_handler.rs +++ b/graph-http/src/blocking/blocking_request_handler.rs @@ -9,8 +9,7 @@ use url::Url; #[derive(Default)] pub struct BlockingRequestHandler { - pub(crate) inner: reqwest::blocking::Client, - pub(crate) access_token: String, + pub(crate) inner: BlockingClient, pub(crate) request_components: RequestComponents, pub(crate) error: Option<GraphFailure>, pub(crate) body: Option<BodyRead>, @@ -23,9 +22,7 @@ impl BlockingRequestHandler { err: Option<GraphFailure>, body: Option<BodyRead>, ) -> BlockingRequestHandler { - let mut original_headers = inner.headers; - original_headers.extend(request_components.headers.clone()); - request_components.headers = original_headers; + request_components.headers.extend(inner.headers.clone()); let mut error = None; if let Some(err) = err { @@ -38,8 +35,7 @@ impl BlockingRequestHandler { } BlockingRequestHandler { - inner: inner.inner.clone(), - access_token: inner.access_token, + inner, request_components, error, body, @@ -137,14 +133,17 @@ impl BlockingRequestHandler { } #[inline] - fn default_request_builder(&mut self) -> reqwest::blocking::RequestBuilder { + fn default_request_builder(&mut self) -> GraphResult<reqwest::blocking::RequestBuilder> { + let access_token = self.inner.client_application.get_token_silent()?; + let request_builder = self + .inner .inner .request( self.request_components.method.clone(), self.request_components.url.clone(), ) - .bearer_auth(self.access_token.as_str()) + .bearer_auth(access_token.as_str()) .headers(self.request_components.headers.clone()); if let Some(body) = self.body.take() { @@ -152,11 +151,11 @@ impl BlockingRequestHandler { .headers .entry(CONTENT_TYPE) .or_insert(HeaderValue::from_static("application/json")); - return request_builder + return Ok(request_builder .body::<reqwest::blocking::Body>(body.into()) - .headers(self.request_components.headers.clone()); + .headers(self.request_components.headers.clone())); } - request_builder + Ok(request_builder) } /// Builds the request and returns a [`reqwest::blocking::RequestBuilder`]. @@ -165,7 +164,7 @@ impl BlockingRequestHandler { if let Some(err) = self.error { return Err(err); } - Ok(self.default_request_builder()) + self.default_request_builder() } #[inline] @@ -250,7 +249,7 @@ impl BlockingPaging { return Err(err); } - let request = self.0.default_request_builder(); + let request = self.0.default_request_builder()?; let response = request.send()?; let (next, http_response) = BlockingPaging::http_response(response)?; @@ -258,8 +257,8 @@ impl BlockingPaging { let mut vec = VecDeque::new(); vec.push_back(http_response); - let client = self.0.inner.clone(); - let access_token = self.0.access_token.clone(); + let client = self.0.inner.inner.clone(); + let access_token = self.0.inner.client_application.get_token_silent()?; while let Some(next) = next_link { let response = client .get(next) @@ -294,15 +293,15 @@ impl BlockingPaging { mut self, ) -> GraphResult<std::sync::mpsc::Receiver<Option<PagingResult<T>>>> { let (sender, receiver) = std::sync::mpsc::channel(); - let request = self.0.default_request_builder(); + let request = self.0.default_request_builder()?; let response = request.send()?; let (next, http_response) = BlockingPaging::http_response(response)?; let mut next_link = next; sender.send(Some(Ok(http_response))).unwrap(); - let client = self.0.inner.clone(); - let access_token = self.0.access_token.clone(); + let client = self.0.inner.inner.clone(); + let access_token = self.0.inner.client_application.get_token_silent()?; std::thread::spawn(move || { while let Some(next) = next_link.as_ref() { diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 6b1f3237..dc1ec7ac 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,10 +1,11 @@ use crate::blocking::BlockingClient; use async_trait::async_trait; use graph_error::AuthExecutionResult; -use graph_extensions::cache::{StoredToken, TokenCacheStore, TokenStore, TokenStoreProvider}; +use graph_extensions::cache::TokenCacheStore; use graph_extensions::token::ClientApplication; -use graph_oauth::identity::{ConfidentialClient, TokenCredentialExecutor}; -use graph_oauth::oauth::{ConfidentialClientApplication, PublicClientApplication}; +use graph_oauth::identity::{ + ConfidentialClientApplication, PublicClientApplication, TokenCredentialExecutor, +}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -21,36 +22,6 @@ fn user_agent_header_from_env() -> Option<HeaderValue> { #[derive(Clone)] pub struct BearerToken(pub String); -impl TokenStore for BearerToken { - fn token_store_provider(&self) -> TokenStoreProvider { - TokenStoreProvider::InMemory - } - - fn is_stored_token_initialized(&self, _id: &str) -> bool { - true - } - - fn get_stored_token(&self, _id: &str) -> Option<&StoredToken> { - None - } - - fn update_stored_token( - &mut self, - _id: &str, - _stored_token: StoredToken, - ) -> Option<StoredToken> { - None - } - - fn get_bearer_token_from_store(&self, _id: &str) -> Option<&String> { - Some(&self.0) - } - - fn get_refresh_token_from_store(&self, _id: &str) -> Option<&String> { - None - } -} - #[async_trait] impl ClientApplication for BearerToken { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { @@ -137,7 +108,7 @@ impl GraphClientConfiguration { Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, >( mut self, - confidential_client: ConfidentialClient<Credential>, + confidential_client: ConfidentialClientApplication<Credential>, ) -> Self { self.config.client_application = Some(Box::new(confidential_client)); self @@ -264,10 +235,18 @@ impl GraphClientConfiguration { builder = builder.connect_timeout(connect_timeout); } - BlockingClient { - access_token: Default::default(), - inner: builder.build().unwrap(), - headers, + if let Some(client_application) = self.config.client_application { + BlockingClient { + client_application, + inner: builder.build().unwrap(), + headers, + } + } else { + BlockingClient { + client_application: Box::new(BearerToken(Default::default())), + inner: builder.build().unwrap(), + headers, + } } } } @@ -338,16 +317,18 @@ impl From<BearerToken> for Client { } } -impl From<PublicClientApplication> for Client { - fn from(value: PublicClientApplication) -> Self { +impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static> + From<PublicClientApplication<Credential>> for Client +{ + fn from(value: PublicClientApplication<Credential>) -> Self { Client::new(value) } } impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static> - From<ConfidentialClient<Credential>> for Client + From<ConfidentialClientApplication<Credential>> for Client { - fn from(value: ConfidentialClient<Credential>) -> Self { + fn from(value: ConfidentialClientApplication<Credential>) -> Self { Client::new(value) } } @@ -355,7 +336,6 @@ impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'sta #[cfg(test)] mod test { use super::*; - use graph_oauth::oauth::ConfidentialClientApplication; #[test] fn compile_time_user_agent_header() { diff --git a/graph-http/src/request_handler.rs b/graph-http/src/request_handler.rs index d6931567..15937b56 100644 --- a/graph-http/src/request_handler.rs +++ b/graph-http/src/request_handler.rs @@ -45,7 +45,7 @@ impl RequestHandler { } RequestHandler { - inner: inner.clone(), + inner, request_components, error, body, diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index e53e2938..5bd2fd90 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -2,7 +2,6 @@ use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::{BTreeSet, HashMap}; use std::default::Default; use std::fmt; -use std::marker::PhantomData; use base64::Engine; use ring::rand::SecureRandom; @@ -959,22 +958,6 @@ impl OAuthSerializer { None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), } } - - #[deprecated] - pub fn build(&mut self) -> GrantSelector<AccessTokenGrant> { - GrantSelector { - oauth: self.clone(), - t: PhantomData, - } - } - - #[deprecated] - pub fn build_async(&mut self) -> GrantSelector<AsyncAccessTokenGrant> { - GrantSelector { - oauth: self.clone(), - t: PhantomData, - } - } } impl OAuthSerializer { @@ -1352,1126 +1335,6 @@ impl<V: ToString> Extend<(OAuthParameter, V)> for OAuthSerializer { } } -#[deprecated] -pub struct GrantSelector<T> { - oauth: OAuthSerializer, - t: PhantomData<T>, -} - -impl GrantSelector<AccessTokenGrant> { - /// Create a new instance for token flow. - /// - /// # See - /// [Microsoft Token Flow Authorization](https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().token_flow(); - /// ``` - pub fn token_flow(self) -> ImplicitGrant { - ImplicitGrant { - oauth: self.oauth, - grant: GrantType::TokenFlow, - } - } - - /// Create a new instance for code flow. - /// - /// # See - /// [Microsoft Code Flow Authorization](https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().code_flow(); - /// ``` - pub fn code_flow(self) -> AccessTokenGrant { - AccessTokenGrant { - oauth: self.oauth, - grant: GrantType::CodeFlow, - } - } - - /// Create a new instance for the implicit grant. - /// - /// # See - /// [Implicit Grant for OAuth 2.0](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().implicit_grant(); - /// ``` - pub fn implicit_grant(self) -> ImplicitGrant { - ImplicitGrant { - oauth: self.oauth, - grant: GrantType::Implicit, - } - } - - /// Create a new instance for authorization code grant. - /// - /// # See - /// [Authorization Code Grant for OAuth 2.0](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().authorization_code_grant(); - /// ``` - pub fn authorization_code_grant(self) -> AccessTokenGrant { - AccessTokenGrant { - oauth: self.oauth, - grant: GrantType::AuthorizationCode, - } - } - - /// Create a new instance for device authorization code grant. - /// - /// # See - /// [Microsoft identity platform and the OAuth 2.0 device authorization grant flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let device_code_handler = oauth.build().device_code(); - /// ``` - pub fn device_code(self) -> DeviceCodeGrant { - DeviceCodeGrant { - oauth: self.oauth, - grant: GrantType::DeviceCode, - } - } - - /// Create a new instance for the open id connect grant. - /// - /// # See - /// [Microsoft Open ID Connect](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().open_id_connect(); - /// ``` - pub fn open_id_connect(self) -> AccessTokenGrant { - AccessTokenGrant { - oauth: self.oauth, - grant: GrantType::OpenId, - } - } - - /// Create a new instance for the client credentials grant. - /// - /// # See - /// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().client_credentials(); - /// ``` - pub fn client_credentials(self) -> AccessTokenGrant { - AccessTokenGrant { - oauth: self.oauth, - grant: GrantType::ClientCredentials, - } - } - - /// Create a new instance for the resource owner password credentials grant. - /// - /// # See - /// [Microsoft Resource Owner Password Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().resource_owner_password_credentials(); - /// ``` - pub fn resource_owner_password_credentials(self) -> AccessTokenGrant { - AccessTokenGrant { - oauth: self.oauth, - grant: GrantType::ResourceOwnerPasswordCredentials, - } - } -} - -impl GrantSelector<AsyncAccessTokenGrant> { - /// Create a new instance for token flow. - /// - /// # See - /// [Microsoft Token Flow Authorization](https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().token_flow(); - /// ``` - pub fn token_flow(self) -> ImplicitGrant { - ImplicitGrant { - oauth: self.oauth, - grant: GrantType::TokenFlow, - } - } - - /// Create a new instance for code flow. - /// - /// # See - /// [Microsoft Code Flow Authorization](https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().code_flow(); - /// ``` - pub fn code_flow(self) -> AsyncAccessTokenGrant { - AsyncAccessTokenGrant { - oauth: self.oauth, - grant: GrantType::CodeFlow, - } - } - - /// Create a new instance for the implicit grant. - /// - /// # See - /// [Implicit Grant for OAuth 2.0](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().implicit_grant(); - /// ``` - pub fn implicit_grant(self) -> ImplicitGrant { - ImplicitGrant { - oauth: self.oauth, - grant: GrantType::Implicit, - } - } - - /// Create a new instance for authorization code grant. - /// - /// # See - /// [Authorization Code Grant for OAuth 2.0](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().authorization_code_grant(); - /// ``` - pub fn authorization_code_grant(self) -> AsyncAccessTokenGrant { - AsyncAccessTokenGrant { - oauth: self.oauth, - grant: GrantType::AuthorizationCode, - } - } - - /// Create a new instance for device authorization code grant. - /// - /// # See - /// [Microsoft identity platform and the OAuth 2.0 device authorization grant flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let device_code_handler = oauth.build().device_code(); - /// ``` - pub fn device_code(self) -> AsyncDeviceCodeGrant { - AsyncDeviceCodeGrant { - oauth: self.oauth, - grant: GrantType::DeviceCode, - } - } - - /// Create a new instance for the open id connect grant. - /// - /// # See - /// [Microsoft Open ID Connect](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().open_id_connect(); - /// ``` - pub fn open_id_connect(self) -> AsyncAccessTokenGrant { - AsyncAccessTokenGrant { - oauth: self.oauth, - grant: GrantType::OpenId, - } - } - - /// Create a new instance for the open id connect grant. - /// - /// # See - /// [Microsoft Client Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().client_credentials(); - /// ``` - pub fn client_credentials(self) -> AsyncAccessTokenGrant { - AsyncAccessTokenGrant { - oauth: self.oauth, - grant: GrantType::ClientCredentials, - } - } - - /// Create a new instance for the resource owner password credentials grant. - /// - /// # See - /// [Microsoft Resource Owner Password Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// let open_id = oauth.build().resource_owner_password_credentials(); - /// ``` - pub fn resource_owner_password_credentials(self) -> AsyncAccessTokenGrant { - AsyncAccessTokenGrant { - oauth: self.oauth, - grant: GrantType::ResourceOwnerPasswordCredentials, - } - } -} - -#[deprecated] -#[derive(Debug)] -pub struct AuthorizationRequest { - uri: String, - error: Option<GraphFailure>, -} - -impl AuthorizationRequest { - pub fn open(self) -> GraphResult<()> { - if self.error.is_some() { - return Err(self.error.unwrap_or_default()); - } - - webbrowser::open(self.uri.as_str()).map_err(GraphFailure::from) - } -} - -#[deprecated] -#[derive(Debug)] -pub struct AccessTokenRequest { - uri: String, - params: HashMap<String, String>, - error: Option<GraphFailure>, -} - -impl AccessTokenRequest { - /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [MsalToken] - /// and pass back to [OAuthSerializer] to use to get refresh tokens. - /// - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::{OAuth, AccessToken}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // As an example create a random access token. - /// let mut access_token = AccessToken::default(); - /// access_token.access_token("12345"); - /// // Store the token in OAuth if the access token has a refresh token. - /// // The refresh token can be later used to request more access tokens. - /// oauth.access_token(access_token); - /// // You can get the actual bearer token if needed: - /// println!("{:#?}", oauth.get_access_token().unwrap().bearer_token()); - /// ``` - /// - /// Request an access token. - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::{AccessToken, OAuth}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // This assumes the user has been authenticated and - /// // the access_code from the request has been given: - /// oauth.access_code("access_code"); - /// - /// // To get an access token a access_token_url is needed and the grant_type - /// // should be set to token. - /// // There are other parameters that may need to be included depending on the - /// // authorization flow chosen. - /// // The url below is for the v1.0 drive API. You can also use the Graph URLs as well. - /// oauth.access_token_url("https://login.live.com/oauth20_token.srf") - /// .response_type("token") - /// .grant_type("authorization_code"); - /// - /// // Make a request for an access token. - /// let mut request = oauth.build().authorization_code_grant(); - /// let response = request.access_token().send()?; - /// println!("{response:#?}"); - /// - /// if response.status().is_success() { - /// let mut access_token: AccessToken = response.json()?; - /// - /// let jwt = access_token.jwt(); - /// println!("{jwt:#?}"); - /// - /// // Store in OAuth for getting refresh tokens. - /// oauth.access_token(access_token); - /// } else { - /// // See if Microsoft Graph returned an error in the Response body - /// let result: reqwest::Result<serde_json::Value> = response.json(); - /// println!("{:#?}", result); - /// } - /// ``` - pub fn send(self) -> GraphResult<reqwest::blocking::Response> { - if self.error.is_some() { - return Err(self.error.unwrap_or_default()); - } - - let client = reqwest::blocking::Client::new(); - client - .post(self.uri.as_str()) - .form(&self.params) - .send() - .map_err(GraphFailure::from) - } -} - -#[deprecated] -#[derive(Debug)] -pub struct AsyncAccessTokenRequest { - uri: String, - params: HashMap<String, String>, - error: Option<GraphFailure>, -} - -impl AsyncAccessTokenRequest { - /// Send the request for an access token. If successful, the Response body - /// should be an access token which you can convert to [MsalToken] - /// and pass back to [OAuthSerializer] to use to get refresh tokens. - /// - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::{OAuth, AccessToken}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // As an example create a random access token. - /// let mut access_token = AccessToken::default(); - /// access_token.access_token("12345"); - /// // Store the token in OAuth if the access token has a refresh token. - /// // The refresh token can be later used to request more access tokens. - /// oauth.access_token(access_token); - /// // You can get the actual bearer token if needed: - /// println!("{:#?}", oauth.get_access_token().unwrap().bearer_token()); - /// ``` - /// - /// Request an access token. - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::{AccessToken, OAuth}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // This assumes the user has been authenticated and - /// // the access_code from the request has been given: - /// oauth.access_code("access_code"); - /// - /// // To get an access token a access_token_url is needed and the grant_type - /// // should be set to token. - /// // There are other parameters that may need to be included depending on the - /// // authorization flow chosen. - /// // The url below is for the v1.0 drive API. You can also use the Graph URLs as well. - /// oauth.access_token_url("https://login.live.com/oauth20_token.srf") - /// .response_type("token") - /// .grant_type("authorization_code"); - /// - /// // Make a request for an access token. - /// let mut request = oauth.build().authorization_code_grant(); - /// let response = request.access_token().send().await?; - /// println!("{response:#?}"); - /// - /// if response.status().is_success() { - /// let mut access_token: AccessToken = response.json().await?; - /// - /// let jwt = access_token.jwt(); - /// println!("{jwt:#?}"); - /// - /// // Store in OAuth for getting refresh tokens. - /// oauth.access_token(access_token); - /// } else { - /// // See if Microsoft Graph returned an error in the Response body - /// let result: reqwest::Result<serde_json::Value> = response.json().await; - /// println!("{:#?}", result); - /// } - /// ``` - pub async fn send(self) -> GraphResult<reqwest::Response> { - if self.error.is_some() { - return Err(self.error.unwrap_or_default()); - } - - let client = reqwest::Client::new(); - client - .post(self.uri.as_str()) - .form(&self.params) - .send() - .await - .map_err(GraphFailure::from) - } -} - -#[deprecated] -#[derive(Debug)] -pub struct ImplicitGrant { - oauth: OAuthSerializer, - grant: GrantType, -} - -impl ImplicitGrant { - pub fn url(&mut self) -> GraphResult<Url> { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - Ok(Url::parse( - self.oauth - .get_or_else(OAuthParameter::AuthorizationUrl)? - .as_str(), - )?) - } - - pub fn browser_authorization(&mut self) -> AuthorizationRequest { - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - ); - - if let Err(e) = params { - return AuthorizationRequest { - uri: Default::default(), - error: Some(e), - }; - } - - let url_result = self.url(); - - if let Err(e) = url_result { - return AuthorizationRequest { - uri: Default::default(), - error: Some(e), - }; - } - - let mut url = url_result.unwrap(); - url.query_pairs_mut().extend_pairs(¶ms.unwrap()); - AuthorizationRequest { - uri: url.to_string(), - error: None, - } - } -} - -impl From<ImplicitGrant> for OAuthSerializer { - fn from(token_grant: ImplicitGrant) -> Self { - token_grant.oauth - } -} - -impl AsRef<OAuthSerializer> for ImplicitGrant { - fn as_ref(&self) -> &OAuthSerializer { - &self.oauth - } -} - -#[deprecated] -pub struct DeviceCodeGrant { - oauth: OAuthSerializer, - grant: GrantType, -} - -impl DeviceCodeGrant { - pub fn authorization_url(&mut self) -> Result<Url, GraphFailure> { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - )?; - let mut url = Url::parse( - self.oauth - .get_or_else(OAuthParameter::AuthorizationUrl)? - .as_str(), - )?; - url.query_pairs_mut().extend_pairs(¶ms); - Ok(url) - } - - pub fn authorization(&mut self) -> AccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let uri = self.oauth.get_or_else(OAuthParameter::AuthorizationUrl); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - ); - - if let Err(e) = uri { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - pub fn access_token(&mut self) -> AccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::AccessToken)); - - if let Err(e) = uri { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - pub fn refresh_token(&mut self) -> AccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::RefreshToken)); - - if let Err(e) = uri { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } -} - -#[deprecated] -pub struct AsyncDeviceCodeGrant { - oauth: OAuthSerializer, - grant: GrantType, -} - -impl AsyncDeviceCodeGrant { - pub fn authorization_url(&mut self) -> Result<Url, GraphFailure> { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - )?; - let mut url = Url::parse( - self.oauth - .get_or_else(OAuthParameter::AuthorizationUrl)? - .as_str(), - )?; - url.query_pairs_mut().extend_pairs(¶ms); - Ok(url) - } - - pub fn authorization(&mut self) -> AsyncAccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let uri = self.oauth.get_or_else(OAuthParameter::AuthorizationUrl); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - ); - - if let Err(e) = uri { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AsyncAccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - pub fn access_token(&mut self) -> AsyncAccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::AccessToken)); - - if let Err(e) = uri { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AsyncAccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - pub fn refresh_token(&mut self) -> AsyncAccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::RefreshToken)); - - if let Err(e) = uri { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AsyncAccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } -} - -#[deprecated] -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct AccessTokenGrant { - oauth: OAuthSerializer, - grant: GrantType, -} - -impl AccessTokenGrant { - pub fn authorization_url(&mut self) -> Result<Url, GraphFailure> { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - )?; - let mut url = Url::parse( - self.oauth - .get_or_else(OAuthParameter::AuthorizationUrl)? - .as_str(), - )?; - url.query_pairs_mut().extend_pairs(¶ms); - Ok(url) - } - - /// Make a request for authorization. The default browser for a user - /// will be opened to the sign in page where the user will need to - /// sign in and agree to any permissions that were set by the provided - /// scopes. - pub fn browser_authorization(&mut self) -> AuthorizationRequest { - let uri = self.authorization_url(); - if let Err(e) = uri { - return AuthorizationRequest { - uri: Default::default(), - error: Some(e), - }; - } - - AuthorizationRequest { - uri: uri.unwrap().to_string(), - error: None, - } - } - - /// Make a request for an access token. The token is stored in OAuth and - /// will be used to make for making requests for refresh tokens. The below - /// example shows how access tokens are stored and retrieved for OAuth: - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::{OAuth, AccessToken}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // As an example create a random access token. - /// let mut access_token = AccessToken::default(); - /// access_token.access_token("12345"); - /// // Store the token in OAuth if the access token has a refresh token. - /// // The refresh token can be later used to request more access tokens. - /// oauth.access_token(access_token); - /// // You can get the actual bearer token if needed: - /// println!("{:#?}", oauth.get_access_token().unwrap().bearer_token()); - /// ``` - /// - /// Request an access token. - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::{AccessToken, OAuth}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // This assumes the user has been authenticated and - /// // the access_code from the request has been given: - /// oauth.access_code("access_code"); - /// - /// // To get an access token a access_token_url is needed and the grant_type - /// // should be set to token. - /// // There are other parameters that may need to be included depending on the - /// // authorization flow chosen. - /// // The url below is for the v1.0 drive API. You can also use the Graph URLs as well. - /// oauth.access_token_url("https://login.live.com/oauth20_token.srf") - /// .response_type("token") - /// .grant_type("authorization_code"); - /// - /// // Make a request for an access token. - /// let mut request = oauth.build().authorization_code_grant(); - /// let response = request.access_token().send()?; - /// println!("{response:#?}"); - /// - /// if response.status().is_success() { - /// let mut access_token: AccessToken = response.json()?; - /// - /// let jwt = access_token.jwt(); - /// println!("{jwt:#?}"); - /// - /// // Store in OAuth for getting refresh tokens. - /// oauth.access_token(access_token); - /// } else { - /// // See if Microsoft Graph returned an error in the Response body - /// let result: reqwest::Result<serde_json::Value> = response.json(); - /// println!("{:#?}", result); - /// } - /// ``` - pub fn access_token(&mut self) -> AccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::AccessToken)); - - if let Err(e) = uri { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - /// Request a refresh token. Assumes an access token has already - /// been retrieved. - pub fn refresh_token(&mut self) -> AccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::RefreshToken)); - - if let Err(e) = uri { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } -} - -#[deprecated] -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct AsyncAccessTokenGrant { - oauth: OAuthSerializer, - grant: GrantType, -} - -impl AsyncAccessTokenGrant { - pub fn authorization_url(&mut self) -> Result<Url, GraphFailure> { - self.oauth - .pre_request_check(self.grant, GrantRequest::Authorization); - let params = self.oauth.params( - self.grant - .available_credentials(GrantRequest::Authorization), - )?; - let mut url = Url::parse( - self.oauth - .get_or_else(OAuthParameter::AuthorizationUrl)? - .as_str(), - )?; - url.query_pairs_mut().extend_pairs(¶ms); - Ok(url) - } - - /// Make a request for authorization. The default browser for a user - /// will be opened to the sign in page where the user will need to - /// sign in and agree to any permissions that were set by the provided - /// scopes. - pub fn browser_authorization(&mut self) -> AuthorizationRequest { - let uri = self.authorization_url(); - if let Err(e) = uri { - return AuthorizationRequest { - uri: Default::default(), - error: Some(e), - }; - } - - AuthorizationRequest { - uri: uri.unwrap().to_string(), - error: None, - } - } - - /// Make a request for an access token. The token is stored in OAuth and - /// will be used to make for making requests for refresh tokens. The below - /// example shows how access tokens are stored and retrieved for OAuth: - /// - /// # Example - /// ```rust,ignore - /// # use graph_oauth::oauth::{OAuth, AccessToken}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // As an example create a random access token. - /// let mut access_token = AccessToken::default(); - /// access_token.access_token("12345"); - /// // Store the token in OAuth if the access token has a refresh token. - /// // The refresh token can be later used to request more access tokens. - /// oauth.access_token(access_token); - /// // You can get the actual bearer token if needed: - /// println!("{:#?}", oauth.get_access_token().unwrap().bearer_token()); - /// ``` - /// - /// Request an access token. - /// # Example - /// ```rust,ignore - /// use graph_oauth::oauth::{AccessToken, OAuth}; - /// let mut oauth: OAuth = OAuth::new(); - /// - /// // This assumes the user has been authenticated and - /// // the access_code from the request has been given: - /// oauth.access_code("access_code"); - /// - /// // To get an access token a access_token_url is needed and the grant_type - /// // should be set to token. - /// // There are other parameters that may need to be included depending on the - /// // authorization flow chosen. - /// // The url below is for the v1.0 drive API. You can also use the Graph URLs as well. - /// oauth.access_token_url("https://login.live.com/oauth20_token.srf") - /// .response_type("token") - /// .grant_type("authorization_code"); - /// - /// // Make a request for an access token. - /// let mut request = oauth.build().authorization_code_grant(); - /// let response = request.access_token().send().await?; - /// println!("{response:#?}"); - /// - /// if response.status().is_success() { - /// let mut access_token: AccessToken = response.json().await?; - /// - /// let jwt = access_token.jwt(); - /// println!("{jwt:#?}"); - /// - /// // Store in OAuth for getting refresh tokens. - /// oauth.access_token(access_token); - /// } else { - /// // See if Microsoft Graph returned an error in the Response body - /// let result: reqwest::Result<serde_json::Value> = response.json().await; - /// println!("{:#?}", result); - /// } - /// ``` - pub fn access_token(&mut self) -> AsyncAccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::AccessToken); - let uri = self.oauth.get_or_else(OAuthParameter::TokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::AccessToken)); - - if let Err(e) = uri { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AsyncAccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } - - /// Request a refresh token. Assumes an access token has already - /// been retrieved. - pub fn refresh_token(&mut self) -> AsyncAccessTokenRequest { - self.oauth - .pre_request_check(self.grant, GrantRequest::RefreshToken); - let uri = self.oauth.get_or_else(OAuthParameter::RefreshTokenUrl); - let params = self - .oauth - .params(self.grant.available_credentials(GrantRequest::RefreshToken)); - - if let Err(e) = uri { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - if let Err(e) = params { - return AsyncAccessTokenRequest { - uri: Default::default(), - params: Default::default(), - error: Some(e), - }; - } - - AsyncAccessTokenRequest { - uri: uri.unwrap(), - params: params.unwrap(), - error: None, - } - } -} - -impl From<AccessTokenGrant> for OAuthSerializer { - fn from(token_grant: AccessTokenGrant) -> Self { - token_grant.oauth - } -} - -impl AsRef<OAuthSerializer> for AccessTokenGrant { - fn as_ref(&self) -> &OAuthSerializer { - &self.oauth - } -} - -impl AsMut<OAuthSerializer> for AccessTokenGrant { - fn as_mut(&mut self) -> &mut OAuthSerializer { - &mut self.oauth - } -} - impl fmt::Debug for OAuthSerializer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut map_debug: BTreeMap<&str, &str> = BTreeMap::new(); diff --git a/graph-oauth/src/identity/authorization_request.rs b/graph-oauth/src/identity/authorization_request.rs new file mode 100644 index 00000000..72e154ab --- /dev/null +++ b/graph-oauth/src/identity/authorization_request.rs @@ -0,0 +1,45 @@ +use http::header::CONTENT_TYPE; +use http::{HeaderMap, HeaderValue}; +use std::collections::HashMap; +use url::Url; + +pub struct AuthorizationRequest { + pub(crate) uri: Url, + pub(crate) form_urlencoded: HashMap<String, String>, + pub(crate) basic_auth: Option<(String, String)>, + pub(crate) headers: HeaderMap, +} + +impl AuthorizationRequest { + pub fn new( + uri: Url, + form_urlencoded: HashMap<String, String>, + basic_auth: Option<(String, String)>, + ) -> AuthorizationRequest { + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-www-form-urlencoded"), + ); + AuthorizationRequest { + uri, + form_urlencoded, + basic_auth, + headers, + } + } + + pub fn with_extra_headers(&mut self, extra_headers: &HeaderMap) { + for (header_name, header_value) in extra_headers.iter() { + self.headers.insert(header_name, header_value.clone()); + } + } + + pub fn with_extra_query_parameters(&mut self, extra_query_params: &HashMap<String, String>) { + for (key, value) in extra_query_params.iter() { + self.uri + .query_pairs_mut() + .append_pair(key.as_ref(), value.as_ref()); + } + } +} diff --git a/graph-oauth/src/identity/cache/in_memory_client_store.rs b/graph-oauth/src/identity/cache/in_memory_client_store.rs deleted file mode 100644 index 609f994e..00000000 --- a/graph-oauth/src/identity/cache/in_memory_client_store.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use crate::oauth::TokenCredentialExecutor; - -#[derive(Clone)] -pub struct InMemoryClientStore<Client: TokenCredentialExecutor, Token> { - client: Box<Client>, - token: Arc<RwLock<Token>>, -} diff --git a/graph-oauth/src/identity/cache/mod.rs b/graph-oauth/src/identity/cache/mod.rs deleted file mode 100644 index 05b040aa..00000000 --- a/graph-oauth/src/identity/cache/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use in_memory_client_store::*; - -mod in_memory_client_store; diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 358502fd..138cb45b 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -6,6 +6,7 @@ use url::Url; use uuid::Uuid; use crate::identity::{Authority, AzureCloudInstance}; +use crate::oauth::ForceTokenRefresh; #[derive(Clone, Debug, Default, PartialEq)] pub struct AppConfig { @@ -18,7 +19,11 @@ pub struct AppConfig { /// The Application (client) ID that the Azure portal - App registrations page assigned /// to your app pub(crate) client_id: Uuid, + /// Specifies which Microsoft accounts can be used for sign-in with a given application. + /// See https://aka.ms/msal-net-application-configuration pub(crate) authority: Authority, + /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). + /// Maps to the instance url string. pub(crate) azure_cloud_instance: AzureCloudInstance, pub(crate) extra_query_parameters: HashMap<String, String>, pub(crate) extra_header_parameters: HeaderMap, @@ -27,7 +32,9 @@ pub struct AppConfig { /// by your app. It must exactly match one of the redirect_uris you registered in the portal, /// except it must be URL-encoded. pub(crate) redirect_uri: Option<Url>, + /// Cache id used in a token cache store. pub(crate) cache_id: String, + pub(crate) force_token_refresh: ForceTokenRefresh, } impl AppConfig { @@ -45,6 +52,7 @@ impl AppConfig { extra_header_parameters: Default::default(), redirect_uri: None, cache_id, + force_token_refresh: Default::default(), } } @@ -75,6 +83,7 @@ impl AppConfig { extra_header_parameters: Default::default(), redirect_uri, cache_id, + force_token_refresh: Default::default(), } } @@ -92,6 +101,7 @@ impl AppConfig { extra_header_parameters: Default::default(), redirect_uri: None, cache_id, + force_token_refresh: Default::default(), } } @@ -116,18 +126,7 @@ impl AppConfig { extra_header_parameters: Default::default(), redirect_uri: None, cache_id, - } - } - - pub(crate) fn cache_id(&self) -> String { - if let Some(tenant_id) = self.tenant_id.as_ref() { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - self.client_id.to_string() - )) - } else { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(self.client_id.to_string()) + force_token_refresh: Default::default(), } } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index bb867a5d..8b35b91b 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,25 +1,23 @@ -use base64::Engine; -use std::collections::HashMap; -use std::env::VarError; - -use http::{HeaderMap, HeaderName, HeaderValue}; -use url::Url; -use uuid::Uuid; - -use graph_error::{IdentityResult, AF}; - use crate::identity::{ application_options::ApplicationOptions, credentials::app_config::AppConfig, credentials::client_assertion_credential::ClientAssertionCredentialBuilder, AuthCodeAuthorizationUrlParameterBuilder, Authority, - AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, - AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, + AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCertificateCredentialBuilder, + AuthorizationCodeCredentialBuilder, AzureCloudInstance, + ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredentialBuilder, }; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; -use crate::oauth::OpenIdAuthorizationUrlBuilder; +use crate::oauth::{OpenIdAuthorizationUrlBuilder, ResourceOwnerPasswordCredential}; +use base64::Engine; +use graph_error::{IdentityResult, AF}; +use http::{HeaderMap, HeaderName, HeaderValue}; +use std::collections::HashMap; +use std::env::VarError; +use url::Url; +use uuid::Uuid; #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum AuthorityHost { @@ -166,8 +164,8 @@ impl ConfidentialClientApplicationBuilder { self, authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, - ) -> AuthorizationCodeCertificateCredentialBuilder { - AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_assertion( + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code_and_assertion( self.into(), authorization_code, assertion, @@ -179,7 +177,7 @@ impl ConfidentialClientApplicationBuilder { self, authorization_code: impl AsRef<str>, x509: &X509Certificate, - ) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( self.into(), authorization_code, @@ -251,6 +249,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { extra_header_parameters: Default::default(), redirect_uri: None, cache_id, + force_token_refresh: Default::default(), }, }) } @@ -263,7 +262,7 @@ pub struct PublicClientApplicationBuilder { impl PublicClientApplicationBuilder { #[allow(dead_code)] - pub fn new(client_id: &str) -> PublicClientApplicationBuilder { + pub fn new(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { PublicClientApplicationBuilder { app_config: AppConfig::new_with_client_id(client_id), } @@ -352,7 +351,8 @@ impl PublicClientApplicationBuilder { ) } - pub fn with_username_password_from_environment() -> Result<PublicClientApplication, VarError> { + pub fn with_username_password_from_environment( + ) -> Result<PublicClientApplication<ResourceOwnerPasswordCredential>, VarError> { EnvironmentCredential::resource_owner_password_credential() } } @@ -402,6 +402,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { extra_header_parameters: Default::default(), redirect_uri: None, cache_id, + force_token_refresh: Default::default(), }, }) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 4371de62..755b60c2 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -7,7 +7,7 @@ use url::Url; use uuid::Uuid; use graph_error::{IdentityResult, AF}; -use graph_extensions::crypto::{secure_random_32, GenPkce, ProofKeyCodeExchange}; +use graph_extensions::crypto::{secure_random_32, ProofKeyCodeExchange}; use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -38,16 +38,14 @@ use crate::identity::{ /// to build the url that the user will be directed to authorize at. /// /// ```rust -/// fn main() { -/// # use graph_oauth::identity::ConfidentialClientApplication; +/// # use graph_oauth::identity::ConfidentialClientApplication;/// +/// +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("access-code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); /// -/// // -/// let client_app = ConfidentialClientApplication::builder("client-id") -/// .with_authorization_code("access-code") -/// .with_client_secret("client-secret") -/// .with_scope(vec!["User.Read"]) -/// .build(); -/// } /// ``` #[derive(Clone)] pub struct AuthCodeAuthorizationUrlParameters { @@ -66,6 +64,11 @@ pub struct AuthCodeAuthorizationUrlParameters { /// - form_post: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub(crate) response_mode: Option<ResponseMode>, + /// A value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + /// The nonce is automatically generated unless set by the caller. pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, /// Required. @@ -131,18 +134,6 @@ impl AuthCodeAuthorizationUrlParameters { response_type.insert(ResponseType::Code); let redirect_uri_result = Url::parse(redirect_uri.as_str()); - /* - AppConfig { - tenant_id: None, - client_id: Uuid::try_parse(client_id.as_ref())?, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: Some(redirect_uri.into_url().or(redirect_uri_result)?), - }, - */ - Ok(AuthCodeAuthorizationUrlParameters { app_config: AppConfig::new_init( Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), @@ -356,6 +347,8 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { if let Some(nonce) = self.nonce.as_ref() { serializer.nonce(nonce); + } else { + serializer.nonce(&secure_random_32()?); } if let Some(code_challenge) = self.code_challenge.as_ref() { @@ -498,28 +491,14 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// resulting id_token as a claim. The app can then verify this value to mitigate token /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. + /// + /// Setting the nonce will override the nonce that is automatically generated by the + /// credential client. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { self.parameters.nonce = Some(nonce.as_ref().to_owned()); self } - /// A value included in the request, generated by the app, that is included in the - /// resulting id_token as a claim. The app can then verify this value to mitigate token - /// replay attacks. The value is typically a randomized, unique string that can be used - /// to identify the origin of the request. - /// - /// The nonce is generated in the same way as generating a PKCE. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - #[doc(hidden)] - pub(crate) fn with_nonce_generated(&mut self) -> IdentityResult<&mut Self> { - self.parameters.nonce = Some(secure_random_32()?); - Ok(self) - } - pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { self.parameters.state = Some(state.as_ref().to_owned()); self @@ -647,7 +626,6 @@ mod test { .unwrap(); let query = url.query().unwrap(); - dbg!(query); assert!(query.contains("response_mode=fragment")); assert!(query.contains("response_type=code+id_token")); } @@ -686,8 +664,6 @@ mod test { .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_nonce_generated() - .unwrap() .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs new file mode 100644 index 00000000..3c86d708 --- /dev/null +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -0,0 +1,310 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; + +use async_trait::async_trait; +use http::{HeaderMap, HeaderName, HeaderValue}; +use reqwest::IntoUrl; +use url::Url; +use uuid::Uuid; + +use graph_error::{IdentityResult, AF}; + +use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{ + AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, + ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, + CLIENT_ASSERTION_TYPE, +}; + +credential_builder!( + AuthorizationCodeAssertionCredentialBuilder, + ConfidentialClientApplication<AuthorizationCodeAssertionCredential> +); + +/// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application +/// to obtain authorized access to protected resources like web APIs. The auth code flow requires +/// a user-agent that supports redirection from the authorization server (the Microsoft +/// identity platform) back to your application. For example, a web browser, desktop, or mobile +/// application operated by a user to sign in to your app and access their data. +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +#[derive(Clone)] +pub struct AuthorizationCodeAssertionCredential { + pub(crate) app_config: AppConfig, + /// The authorization code obtained from a call to authorize. The code should be obtained with all required scopes. + pub(crate) authorization_code: Option<String>, + /// The refresh token needed to make an access token request using a refresh token. + /// Do not include an authorization code when using a refresh token. + pub(crate) refresh_token: Option<String>, + /// The same code_verifier that was used to obtain the authorization_code. + /// Required if PKCE was used in the authorization code grant request. For more information, + /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. + pub(crate) code_verifier: Option<String>, + /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + pub(crate) client_assertion_type: String, + /// An assertion (a JSON web token) that you need to create and sign with the certificate + /// you registered as credentials for your application. Read about certificate credentials + /// to learn how to register your certificate and the format of the assertion. + pub(crate) client_assertion: String, + /// Required + /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the + /// scope openid, which translates to the "Sign you in" permission in the consent UI. + /// Optionally you may also want to include the email and profile scopes for gaining access + /// to additional user data. You may also include other scopes in this request for requesting + /// consent to various resources, if an access token is requested. + pub(crate) scope: Vec<String>, + serializer: OAuthSerializer, +} + +impl Debug for AuthorizationCodeAssertionCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AuthorizationCodeAssertionCredential") + .field("app_config", &self.app_config) + .field("scope", &self.scope) + .finish() + } +} +impl AuthorizationCodeAssertionCredential { + pub fn new<T: AsRef<str>, U: IntoUrl>( + client_id: T, + authorization_code: T, + client_assertion: T, + redirect_uri: Option<U>, + ) -> IdentityResult<AuthorizationCodeAssertionCredential> { + let redirect_uri = { + if let Some(redirect_uri) = redirect_uri { + redirect_uri.into_url().ok() + } else { + None + } + }; + + Ok(AuthorizationCodeAssertionCredential { + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + redirect_uri, + ), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: client_assertion.as_ref().to_owned(), + scope: vec![], + serializer: OAuthSerializer::new(), + }) + } + + pub fn builder( + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + Default::default(), + authorization_code, + ) + } + + pub fn authorization_url_builder<T: AsRef<str>>( + client_id: T, + ) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new(client_id) + } +} + +#[async_trait] +impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { + fn uri(&mut self) -> IdentityResult<Url> { + let azure_cloud_instance = self.azure_cloud_instance(); + self.serializer + .authority(&azure_cloud_instance, &self.authority()); + + let uri = self + .serializer + .get(OAuthParameter::TokenUrl) + .ok_or(AF::msg_internal_err("token_url"))?; + Url::parse(uri.as_str()).map_err(AF::from) + } + + fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let client_id = self.app_config.client_id.to_string(); + if client_id.is_empty() || self.app_config.client_id.is_nil() { + return AF::result(OAuthParameter::ClientId); + } + + if self.client_assertion.trim().is_empty() { + return AF::result(OAuthParameter::ClientAssertion); + } + + if self.client_assertion_type.trim().is_empty() { + self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); + } + + self.serializer + .client_id(client_id.as_str()) + .client_assertion(self.client_assertion.as_str()) + .client_assertion_type(self.client_assertion_type.as_str()) + .extend_scopes(self.scope.clone()); + + if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { + self.serializer.redirect_uri(redirect_uri.as_str()); + } + + if let Some(code_verifier) = self.code_verifier.as_ref() { + self.serializer.code_verifier(code_verifier.as_ref()); + } + + if let Some(refresh_token) = self.refresh_token.as_ref() { + if refresh_token.trim().is_empty() { + return AF::msg_result( + OAuthParameter::RefreshToken.alias(), + "refresh_token is empty - cannot be an empty string", + ); + } + + self.serializer + .refresh_token(refresh_token.as_ref()) + .grant_type("refresh_token"); + + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::RefreshToken, + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ); + } else if let Some(authorization_code) = self.authorization_code.as_ref() { + if authorization_code.trim().is_empty() { + return AF::msg_result( + OAuthParameter::AuthorizationCode.alias(), + "authorization_code is empty - cannot be an empty string", + ); + } + + self.serializer + .authorization_code(authorization_code.as_str()) + .grant_type("authorization_code"); + + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![ + OAuthParameter::AuthorizationCode, + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::RedirectUri, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ); + } + + AF::msg_result( + format!( + "{} or {}", + OAuthParameter::AuthorizationCode.alias(), + OAuthParameter::RefreshToken.alias() + ), + "Either authorization code or refresh token is required", + ) + } + + fn client_id(&self) -> &Uuid { + &self.app_config.client_id + } + + fn authority(&self) -> Authority { + self.app_config.authority.clone() + } + + fn azure_cloud_instance(&self) -> AzureCloudInstance { + self.app_config.azure_cloud_instance + } + + fn app_config(&self) -> &AppConfig { + &self.app_config + } +} + +#[derive(Clone)] +pub struct AuthorizationCodeAssertionCredentialBuilder { + credential: AuthorizationCodeAssertionCredential, +} + +impl AuthorizationCodeAssertionCredentialBuilder { + pub(crate) fn new_with_auth_code( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + Self { + credential: AuthorizationCodeAssertionCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + scope: vec![], + serializer: OAuthSerializer::new(), + }, + } + } + + pub(crate) fn new_with_auth_code_and_assertion( + app_config: AppConfig, + authorization_code: impl AsRef<str>, + assertion: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + Self { + credential: AuthorizationCodeAssertionCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: assertion.as_ref().to_owned(), + scope: vec![], + serializer: OAuthSerializer::new(), + }, + } + } + + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { + self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); + self + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { + self.credential.authorization_code = None; + self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); + self + } + + pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); + Ok(self) + } + + pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { + self.credential.code_verifier = Some(code_verifier.as_ref().to_owned()); + self + } + + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + self.credential.client_assertion = client_assertion.as_ref().to_owned(); + self + } + + pub fn with_client_assertion_type<T: AsRef<str>>( + &mut self, + client_assertion_type: T, + ) -> &mut Self { + self.credential.client_assertion_type = client_assertion_type.as_ref().to_owned(); + self + } + + pub fn credential(self) -> AuthorizationCodeAssertionCredential { + self.credential + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 9eff37c9..b07d6f3c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -13,7 +13,7 @@ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, + AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; #[cfg(feature = "openssl")] @@ -21,7 +21,7 @@ use crate::oauth::X509Certificate; credential_builder!( AuthorizationCodeCertificateCredentialBuilder, - ConfidentialClientApplication + ConfidentialClientApplication<AuthorizationCodeCertificateCredential> ); /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application @@ -97,12 +97,16 @@ impl AuthorizationCodeCertificateCredential { }) } + #[cfg(feature = "openssl")] pub fn builder( + client_id: impl AsRef<str>, authorization_code: impl AsRef<str>, - ) -> AuthorizationCodeCertificateCredentialBuilder { - AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code( - Default::default(), + x509: &X509Certificate, + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + AppConfig::new_with_client_id(client_id), authorization_code, + x509, ) } @@ -250,49 +254,12 @@ impl AuthorizationCodeCertificateCredentialBuilder { } } - pub(crate) fn new_with_auth_code( - app_config: AppConfig, - authorization_code: impl AsRef<str>, - ) -> AuthorizationCodeCertificateCredentialBuilder { - Self { - credential: AuthorizationCodeCertificateCredential { - app_config, - authorization_code: Some(authorization_code.as_ref().to_owned()), - refresh_token: None, - code_verifier: None, - client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: String::new(), - scope: vec![], - serializer: OAuthSerializer::new(), - }, - } - } - - pub(crate) fn new_with_auth_code_and_assertion( - app_config: AppConfig, - authorization_code: impl AsRef<str>, - assertion: impl AsRef<str>, - ) -> AuthorizationCodeCertificateCredentialBuilder { - Self { - credential: AuthorizationCodeCertificateCredential { - app_config, - authorization_code: Some(authorization_code.as_ref().to_owned()), - refresh_token: None, - code_verifier: None, - client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: assertion.as_ref().to_owned(), - scope: vec![], - serializer: OAuthSerializer::new(), - }, - } - } - #[cfg(feature = "openssl")] pub(crate) fn new_with_auth_code_and_x509( app_config: AppConfig, authorization_code: impl AsRef<str>, x509: &X509Certificate, - ) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { let mut builder = Self { credential: AuthorizationCodeCertificateCredential { app_config, @@ -335,7 +302,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { pub fn with_x509( &mut self, certificate_assertion: &X509Certificate, - ) -> anyhow::Result<&mut Self> { + ) -> IdentityResult<&mut Self> { if let Some(tenant_id) = self.credential.authority().tenant_id() { self.with_client_assertion( certificate_assertion.sign_with_tenant(Some(tenant_id.clone()))?, diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index f242320a..8aa543da 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -7,19 +7,22 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_error::{IdentityResult, AF}; +use graph_error::{AuthExecutionError, IdentityResult, AF}; +use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; use graph_extensions::crypto::ProofKeyCodeExchange; +use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, TokenCredentialExecutor, + Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, + TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; credential_builder!( AuthorizationCodeCredentialBuilder, - ConfidentialClientApplication + ConfidentialClientApplication<AuthorizationCodeCredential> ); /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application @@ -60,6 +63,7 @@ pub struct AuthorizationCodeCredential { /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, serializer: OAuthSerializer, + token_cache: InMemoryCredentialStore<MsalToken>, } impl Debug for AuthorizationCodeCredential { @@ -71,6 +75,63 @@ impl Debug for AuthorizationCodeCredential { } } +#[async_trait] +impl TokenCacheStore for AuthorizationCodeCredential { + type Token = MsalToken; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + Ok(msal_token) + } + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + impl AuthorizationCodeCredential { pub fn new<T: AsRef<str>, U: IntoUrl>( tenant_id: T, @@ -86,6 +147,7 @@ impl AuthorizationCodeCredential { scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }) } @@ -111,6 +173,7 @@ impl AuthorizationCodeCredential { scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }) } @@ -153,6 +216,7 @@ impl AuthorizationCodeCredentialBuilder { scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, } } @@ -170,6 +234,7 @@ impl AuthorizationCodeCredentialBuilder { scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, } } @@ -242,6 +307,25 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { .client_secret(self.client_secret.as_str()) .extend_scopes(self.scope.clone()); + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.serializer + .grant_type("refresh_token") + .refresh_token(refresh_token.as_ref()); + + return self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::ClientSecret, + OAuthParameter::RefreshToken, + OAuthParameter::GrantType, + ], + ); + } + } + if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index cff94270..98f3b280 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -6,17 +6,20 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; -use graph_error::{IdentityResult, AF}; +use graph_error::{AuthExecutionError, IdentityResult, AF}; +use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::token::MsalToken; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, + Authority, AzureCloudInstance, ForceTokenRefresh, TokenCredentialExecutor, + CLIENT_ASSERTION_TYPE, }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; credential_builder!( ClientAssertionCredentialBuilder, - ConfidentialClientApplication + ConfidentialClientApplication<ClientAssertionCredential> ); #[derive(Clone)] @@ -31,6 +34,7 @@ pub struct ClientAssertionCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, + token_cache: InMemoryCredentialStore<MsalToken>, } impl ClientAssertionCredential { @@ -46,6 +50,7 @@ impl ClientAssertionCredential { client_assertion: assertion.as_ref().to_string(), refresh_token: None, serializer: Default::default(), + token_cache: Default::default(), } } } @@ -59,6 +64,49 @@ impl Debug for ClientAssertionCredential { } } +#[async_trait] +impl TokenCacheStore for ClientAssertionCredential { + type Token = MsalToken; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + #[derive(Clone)] pub struct ClientAssertionCredentialBuilder { credential: ClientAssertionCredential, @@ -75,6 +123,7 @@ impl ClientAssertionCredentialBuilder { client_assertion: Default::default(), refresh_token: None, serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -91,6 +140,7 @@ impl ClientAssertionCredentialBuilder { client_assertion: signed_assertion, refresh_token: None, serializer: Default::default(), + token_cache: Default::default(), }, } } diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 4dd735cc..26f3bf81 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -88,6 +88,14 @@ macro_rules! credential_builder_base { .extend(header_parameters); self } + + pub fn force_token_refresh( + &mut self, + force_token_refresh: ForceTokenRefresh, + ) -> &mut Self { + self.credential.app_config.force_token_refresh = force_token_refresh; + self + } } }; } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 18f524f2..4c988c5c 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -6,21 +6,25 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult, AF}; +use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; -use crate::oauth::{ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication}; +use crate::identity::{ + Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, + ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, +}; pub(crate) static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; credential_builder!( ClientCertificateCredentialBuilder, - ConfidentialClientApplication + ConfidentialClientApplication<ClientCertificateCredential> ); /// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials @@ -37,6 +41,7 @@ pub struct ClientCertificateCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, + token_cache: InMemoryCredentialStore<MsalToken>, } impl ClientCertificateCredential { @@ -48,6 +53,7 @@ impl ClientCertificateCredential { client_assertion: client_assertion.as_ref().to_owned(), refresh_token: None, serializer: Default::default(), + token_cache: Default::default(), } } @@ -85,6 +91,50 @@ impl Debug for ClientCertificateCredential { .finish() } } + +#[async_trait] +impl TokenCacheStore for ClientCertificateCredential { + type Token = MsalToken; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let msal_token: MsalToken = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: MsalToken = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { fn uri(&mut self) -> IdentityResult<Url> { @@ -192,6 +242,7 @@ impl ClientCertificateCredentialBuilder { client_assertion: String::new(), refresh_token: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, } } @@ -209,6 +260,7 @@ impl ClientCertificateCredentialBuilder { client_assertion: String::new(), refresh_token: None, serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, }; credential_builder.with_certificate(x509)?; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index c93d2184..4336b80d 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -11,13 +11,16 @@ use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, ConfidentialClient, - ConfidentialClientApplication, TokenCredentialExecutor, + credentials::app_config::AppConfig, Authority, AzureCloudInstance, + ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication, ForceTokenRefresh, + TokenCredentialExecutor, }; -credential_builder!(ClientSecretCredentialBuilder, ConfidentialClientApplication); +credential_builder!( + ClientSecretCredentialBuilder, + ConfidentialClientApplication<ClientSecretCredential> +); /// Client Credentials flow using a client secret. /// @@ -99,13 +102,13 @@ impl TokenCacheStore for ClientSecretCredential { fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired() { + if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute()?; let msal_token: MsalToken = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { - Ok(token.clone()) + Ok(token) } } else { let response = self.execute()?; @@ -115,20 +118,24 @@ impl TokenCacheStore for ClientSecretCredential { } } + #[tracing::instrument] async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired() { + if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute_async().await?; let msal_token: MsalToken = response.json().await?; + tracing::debug!("tokenResponse={:#?}", &msal_token); self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { + tracing::debug!("tokenResponse={:#?}", &token); Ok(token.clone()) } } else { let response = self.execute_async().await?; let msal_token: MsalToken = response.json().await?; + tracing::debug!("tokenResponse={:#?}", &msal_token); self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } @@ -236,8 +243,8 @@ impl ClientSecretCredentialBuilder { self } - pub fn build_client(&self) -> ConfidentialClient<ClientSecretCredential> { - ConfidentialClient::new(self.credential.clone()) + pub fn build_client(&self) -> ConfidentialClientApplication<ClientSecretCredential> { + ConfidentialClientApplication::credential(self.credential.clone()) } pub fn credential(&self) -> ClientSecretCredential { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index aec4abf3..cb5bddea 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -1,141 +1,25 @@ use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; +use std::fmt::Debug; use async_trait::async_trait; -use dyn_clone::DynClone; -use reqwest::tls::Version; -use reqwest::{ClientBuilder, Response}; + +use reqwest::Response; use url::Url; use uuid::Uuid; use graph_error::{AuthExecutionResult, IdentityResult}; -use graph_extensions::cache::{AsBearer, AutomaticTokenRefresh, TokenCacheStore, TokenStore}; +use graph_extensions::cache::{AsBearer, TokenCacheStore}; use graph_extensions::token::ClientApplication; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; -use crate::identity::credentials::client_assertion_credential::ClientAssertionCredential; use crate::identity::{ - Authority, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, - AzureCloudInstance, ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, - TokenCredentialExecutor, + credentials::app_config::AppConfig, + credentials::application_builder::ConfidentialClientApplicationBuilder, + credentials::client_assertion_credential::ClientAssertionCredential, Authority, + AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, + AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, + ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, }; -pub struct ClientCache {} - -#[derive(Clone, Debug)] -pub struct ConfidentialClient<Credential> { - credential: Credential, -} - -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> ConfidentialClient<Credential> { - pub fn new(credential: Credential) -> ConfidentialClient<Credential> { - ConfidentialClient { credential } - } - - pub fn credential(credential: Credential) -> ConfidentialClient<Credential> { - ConfidentialClient { credential } - } - - pub fn builder(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { - ConfidentialClientApplicationBuilder::new(client_id) - } -} - -#[async_trait] -impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication - for ConfidentialClient<Credential> -{ - fn get_token_silent(&mut self) -> AuthExecutionResult<String> { - let token = self.credential.get_token_silent()?; - Ok(token.as_bearer()) - } - - async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { - let token = self.credential.get_token_silent_async().await?; - Ok(token.as_bearer()) - } -} - -#[async_trait] -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor - for ConfidentialClient<Credential> -{ - fn uri(&mut self) -> IdentityResult<Url> { - self.credential.uri() - } - - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - self.credential.form_urlencode() - } - - fn client_id(&self) -> &Uuid { - self.credential.client_id() - } - - fn authority(&self) -> Authority { - self.credential.authority() - } - - fn azure_cloud_instance(&self) -> AzureCloudInstance { - self.credential.azure_cloud_instance() - } - - fn basic_auth(&self) -> Option<(String, String)> { - self.credential.basic_auth() - } - - fn app_config(&self) -> &AppConfig { - self.credential.app_config() - } - - fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - self.credential.execute() - } - - async fn execute_async(&mut self) -> AuthExecutionResult<Response> { - self.credential.execute_async().await - } -} - -impl From<AuthorizationCodeCredential> for ConfidentialClient<AuthorizationCodeCredential> { - fn from(value: AuthorizationCodeCredential) -> Self { - ConfidentialClient::new(value) - } -} - -impl From<AuthorizationCodeCertificateCredential> - for ConfidentialClient<AuthorizationCodeCertificateCredential> -{ - fn from(value: AuthorizationCodeCertificateCredential) -> Self { - ConfidentialClient::credential(value) - } -} - -impl From<ClientSecretCredential> for ConfidentialClient<ClientSecretCredential> { - fn from(value: ClientSecretCredential) -> Self { - ConfidentialClient::credential(value) - } -} - -impl From<ClientCertificateCredential> for ConfidentialClient<ClientCertificateCredential> { - fn from(value: ClientCertificateCredential) -> Self { - ConfidentialClient::credential(value) - } -} - -impl From<ClientAssertionCredential> for ConfidentialClient<ClientAssertionCredential> { - fn from(value: ClientAssertionCredential) -> Self { - ConfidentialClient::credential(value) - } -} - -impl From<OpenIdCredential> for ConfidentialClient<OpenIdCredential> { - fn from(value: OpenIdCredential) -> Self { - ConfidentialClient::credential(value) - } -} - /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. @@ -148,187 +32,52 @@ impl From<OpenIdCredential> for ConfidentialClient<OpenIdCredential> { /// to build the url that the user will be directed to authorize at. /// /// ```rust -/// fn main() { -/// # use graph_oauth::identity::ConfidentialClientApplication; -/// -/// // -/// let client_app = ConfidentialClientApplication::builder("client-id") -/// .with_authorization_code("access-code") -/// .with_client_secret("client-secret") -/// .with_scope(vec!["User.Read"]) -/// .build(); -/// } -/// ``` -#[derive(Clone)] -pub struct ConfidentialClientApplication { - http_client: reqwest::Client, - credential: Box<dyn TokenCredentialExecutor + Send>, +#[derive(Clone, Debug)] +pub struct ConfidentialClientApplication<Credential> { + credential: Credential, } -impl Debug for ConfidentialClientApplication { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConfidentialClientApplication") - .field("credential", &self.credential) - .finish() +impl ConfidentialClientApplication<()> { + pub fn builder(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { + ConfidentialClientApplicationBuilder::new(client_id) } } -impl ConfidentialClientApplication { - pub(crate) fn new<T>(credential: T) -> ConfidentialClientApplication - where - T: Into<ConfidentialClientApplication>, - { - credential.into() - } - - pub(crate) fn credential<T>(credential: T) -> ConfidentialClientApplication - where - T: TokenCredentialExecutor + Send + 'static, - { - let (token_sender, token_watch) = AutomaticTokenRefresh::new(String::new()); - - ConfidentialClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - credential: Box::new(credential), - } +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> + ConfidentialClientApplication<Credential> +{ + pub(crate) fn new(credential: Credential) -> ConfidentialClientApplication<Credential> { + ConfidentialClientApplication { credential } } - pub fn builder(client_id: &str) -> ConfidentialClientApplicationBuilder { - ConfidentialClientApplicationBuilder::new(client_id) + pub(crate) fn credential(credential: Credential) -> ConfidentialClientApplication<Credential> { + ConfidentialClientApplication { credential } } - /* - fn openid_userinfo(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let response = self.get_openid_config()?; - let config: serde_json::Value = response.json()?; - let user_info_endpoint = Url::parse(config["userinfo_endpoint"].as_str().unwrap()).unwrap(); - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - - let cache_id = self.app_config().cache_id(); - let bearer = self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::msg_err( - "TokenStore", - "User Info endpoint requires bearer token - no bearer token found in token cache store", - ))?; - - let response = http_client - .get(user_info_endpoint) - .headers(headers) - .bearer_auth(bearer) - .send() - .expect("Error on header"); - - Ok(response) + pub fn into_inner(self) -> Credential { + self.credential } - */ } -/* #[async_trait] -impl ClientApplication for ConfidentialClientApplication { +impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication + for ConfidentialClientApplication<Credential> +{ fn get_token_silent(&mut self) -> AuthExecutionResult<String> { - let cache_id = self.app_config().cache_id(); - if self.is_store_and_token_initialized(cache_id.as_str()) { - return Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error getting token from store - please report issue", - ))? - .clone()); - } - - if !self.is_token_store_initialized() { - self.with_in_memory_token_store(); - } - - let response = self.execute()?; - let msal_token: MsalToken = response.json()?; - self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); - Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error initializing token store - please report issue", - ))? - .clone()) + let token = self.credential.get_token_silent()?; + Ok(token.as_bearer()) } async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { - let cache_id = self.app_config().cache_id(); - if self.is_store_and_token_initialized(cache_id.as_str()) { - return Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error getting token from store - please report issue", - ))? - .clone()); - } - - if !self.is_token_store_initialized() { - self.with_in_memory_token_store(); - } - - let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; - self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); - Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error initializing token store - please report issue", - ))? - .clone()) - } - - fn get_stored_application_token(&mut self) -> Option<&StoredToken> { - let cache_id = self.app_config().cache_id(); - if !self.is_store_and_token_initialized(cache_id.as_str()) { - self.get_token_silent().ok()?; - } - - self.token_store.get_stored_token(cache_id.as_str()) - } -} - */ - -/* -impl TokenStore for ConfidentialClientApplication { - fn token_store_provider(&self) -> TokenStoreProvider { - self.token_store.token_store_provider() - } - - fn is_stored_token_initialized(&self, id: &str) -> bool { - self.token_store.is_stored_token_initialized(id) - } - - fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { - self.token_store.get_stored_token(id) - } - - fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { - self.token_store.update_stored_token(id, stored_token) - } - - fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.get_bearer_token_from_store(id) - } - - fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.get_refresh_token_from_store(id) + let token = self.credential.get_token_silent_async().await?; + Ok(token.as_bearer()) } } - */ #[async_trait] -impl TokenCredentialExecutor for ConfidentialClientApplication { +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor + for ConfidentialClientApplication<Credential> +{ fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() } @@ -366,37 +115,51 @@ impl TokenCredentialExecutor for ConfidentialClientApplication { } } -impl From<AuthorizationCodeCredential> for ConfidentialClientApplication { +impl From<AuthorizationCodeCredential> + for ConfidentialClientApplication<AuthorizationCodeCredential> +{ fn from(value: AuthorizationCodeCredential) -> Self { ConfidentialClientApplication::credential(value) } } -impl From<AuthorizationCodeCertificateCredential> for ConfidentialClientApplication { +impl From<AuthorizationCodeAssertionCredential> + for ConfidentialClientApplication<AuthorizationCodeAssertionCredential> +{ + fn from(value: AuthorizationCodeAssertionCredential) -> Self { + ConfidentialClientApplication::credential(value) + } +} + +impl From<AuthorizationCodeCertificateCredential> + for ConfidentialClientApplication<AuthorizationCodeCertificateCredential> +{ fn from(value: AuthorizationCodeCertificateCredential) -> Self { ConfidentialClientApplication::credential(value) } } -impl From<ClientSecretCredential> for ConfidentialClientApplication { +impl From<ClientSecretCredential> for ConfidentialClientApplication<ClientSecretCredential> { fn from(value: ClientSecretCredential) -> Self { ConfidentialClientApplication::credential(value) } } -impl From<ClientCertificateCredential> for ConfidentialClientApplication { +impl From<ClientCertificateCredential> + for ConfidentialClientApplication<ClientCertificateCredential> +{ fn from(value: ClientCertificateCredential) -> Self { ConfidentialClientApplication::credential(value) } } -impl From<ClientAssertionCredential> for ConfidentialClientApplication { +impl From<ClientAssertionCredential> for ConfidentialClientApplication<ClientAssertionCredential> { fn from(value: ClientAssertionCredential) -> Self { ConfidentialClientApplication::credential(value) } } -impl From<OpenIdCredential> for ConfidentialClientApplication { +impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> { fn from(value: OpenIdCredential) -> Self { ConfidentialClientApplication::credential(value) } @@ -472,76 +235,4 @@ mod test { credential_uri.as_str() ); } - - /* - #[test] - fn in_memory_token_store_init() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - client_id_string, - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent().unwrap(), - "token".to_string() - ) - } - - #[tokio::test] - async fn in_memory_token_store_init_async() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - client_id_string, - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent_async().await.unwrap(), - "token".to_string() - ) - } - - #[tokio::test] - async fn in_memory_token_store_tenant_and_client_cache_id() { - let client_id = Uuid::new_v4(); - let client_id_string = client_id.to_string(); - let mut confidential_client = - ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") - .with_tenant("tenant-id") - .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() - .build(); - - confidential_client.token_store = Box::new(InMemoryCredentialStore::new( - format!("{},{}", "tenant-id", client_id_string), - StoredToken::BearerToken("token".into()), - )); - assert_eq!( - confidential_client.get_token_silent_async().await.unwrap(), - "token".to_string() - ) - } - */ } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index bbecb2b3..251c3611 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -18,12 +18,17 @@ use graph_extensions::http::{ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; -use crate::oauth::{DeviceCode, PollDeviceCodeType, PublicClientApplication}; +use crate::identity::{ + Authority, AzureCloudInstance, DeviceCode, ForceTokenRefresh, PollDeviceCodeType, + PublicClientApplication, TokenCredentialExecutor, +}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; -credential_builder!(DeviceCodeCredentialBuilder, PublicClientApplication); +credential_builder!( + DeviceCodeCredentialBuilder, + PublicClientApplication<DeviceCodeCredential> +); /// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, /// or a printer. To enable this flow, the device has the user visit a webpage in a browser on @@ -373,7 +378,7 @@ impl DeviceCodePollingExecutor { let mut interval = Duration::from_secs(device_code_response.interval); credential.with_device_code(device_code); - let _ = tokio::spawn(async move { + tokio::spawn(async move { let mut should_slow_down = false; loop { @@ -425,7 +430,7 @@ impl DeviceCodePollingExecutor { } } } - return Ok::<(), anyhow::Error>(()); + Ok::<(), anyhow::Error>(()) }); Ok(receiver) diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 455fc1d8..6faf7ea0 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -33,22 +33,24 @@ impl Debug for EnvironmentCredential { } impl EnvironmentCredential { - pub fn resource_owner_password_credential() -> Result<PublicClientApplication, VarError> { + pub fn resource_owner_password_credential( + ) -> Result<PublicClientApplication<ResourceOwnerPasswordCredential>, VarError> { match EnvironmentCredential::try_username_password_compile_time_env() { Ok(credential) => Ok(credential), Err(_) => EnvironmentCredential::try_username_password_runtime_env(), } } - pub fn client_secret_credential() -> Result<ConfidentialClientApplication, VarError> { + pub fn client_secret_credential( + ) -> Result<ConfidentialClientApplication<ClientSecretCredential>, VarError> { match EnvironmentCredential::try_azure_client_secret_compile_time_env() { Ok(credential) => Ok(credential), Err(_) => EnvironmentCredential::try_azure_client_secret_runtime_env(), } } - fn try_azure_client_secret_compile_time_env() -> Result<ConfidentialClientApplication, VarError> - { + fn try_azure_client_secret_compile_time_env( + ) -> Result<ConfidentialClientApplication<ClientSecretCredential>, VarError> { let tenant_id = option_env!("AZURE_TENANT_ID"); let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_client_secret = option_env!("AZURE_CLIENT_SECRET").ok_or(VarError::NotPresent)?; @@ -59,7 +61,8 @@ impl EnvironmentCredential { ) } - fn try_azure_client_secret_runtime_env() -> Result<ConfidentialClientApplication, VarError> { + fn try_azure_client_secret_runtime_env( + ) -> Result<ConfidentialClientApplication<ClientSecretCredential>, VarError> { let tenant_id = std::env::var(AZURE_TENANT_ID).ok(); let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_client_secret = std::env::var(AZURE_CLIENT_SECRET)?; @@ -70,22 +73,23 @@ impl EnvironmentCredential { tenant_id: Option<String>, azure_client_id: String, azure_client_secret: String, - ) -> Result<ConfidentialClientApplication, VarError> { + ) -> Result<ConfidentialClientApplication<ClientSecretCredential>, VarError> { match tenant_id { - Some(tenant_id) => Ok(ConfidentialClientApplication::new( + Some(tenant_id) => Ok(ConfidentialClientApplication::credential( ClientSecretCredential::new_with_tenant( tenant_id, azure_client_id, azure_client_secret, ), )), - None => Ok(ConfidentialClientApplication::new( + None => Ok(ConfidentialClientApplication::credential( ClientSecretCredential::new(azure_client_id, azure_client_secret), )), } } - fn try_username_password_compile_time_env() -> Result<PublicClientApplication, VarError> { + fn try_username_password_compile_time_env( + ) -> Result<PublicClientApplication<ResourceOwnerPasswordCredential>, VarError> { let tenant_id = option_env!("AZURE_TENANT_ID"); let azure_client_id = option_env!("AZURE_CLIENT_ID").ok_or(VarError::NotPresent)?; let azure_username = option_env!("AZURE_USERNAME").ok_or(VarError::NotPresent)?; @@ -98,7 +102,8 @@ impl EnvironmentCredential { )) } - fn try_username_password_runtime_env() -> Result<PublicClientApplication, VarError> { + fn try_username_password_runtime_env( + ) -> Result<PublicClientApplication<ResourceOwnerPasswordCredential>, VarError> { let tenant_id = std::env::var(AZURE_TENANT_ID).ok(); let azure_client_id = std::env::var(AZURE_CLIENT_ID)?; let azure_username = std::env::var(AZURE_USERNAME)?; @@ -116,7 +121,7 @@ impl EnvironmentCredential { azure_client_id: String, azure_username: String, azure_password: String, - ) -> PublicClientApplication { + ) -> PublicClientApplication<ResourceOwnerPasswordCredential> { match tenant_id { Some(tenant_id) => { PublicClientApplication::new(ResourceOwnerPasswordCredential::new_with_tenant( diff --git a/graph-oauth/src/identity/credentials/force_token_refresh.rs b/graph-oauth/src/identity/credentials/force_token_refresh.rs new file mode 100644 index 00000000..86584baf --- /dev/null +++ b/graph-oauth/src/identity/credentials/force_token_refresh.rs @@ -0,0 +1,14 @@ +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ForceTokenRefresh { + /// Always use the token cache first to when returning tokens. + /// Expired tokens will still cause an authorization request to + /// be called. + #[default] + Never, + /// ForceRefreshToken::Once will cause only the next authorization request + /// to ignore any tokens in cache and request a new token. Authorization + /// requests after this are treated as ForceRefreshToken::Never + Once, + /// Always make an authorization request regardless of any tokens in cache. + Always, +} diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index d62587fa..65895f20 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,5 +1,5 @@ use graph_error::{AuthorizationFailure, IdentityResult}; -use graph_extensions::crypto::{secure_random_32, GenPkce}; +use graph_extensions::crypto::secure_random_32; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -9,7 +9,7 @@ use uuid::*; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; +use crate::identity::{AzureCloudInstance, ForceTokenRefresh, Prompt, ResponseMode, ResponseType}; credential_builder_base!(ImplicitCredentialBuilder); diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index e4e45262..32d300f8 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,6 +1,7 @@ pub use application_builder::*; pub use as_query::*; pub use auth_code_authorization_url::*; +pub use authorization_code_assertion_credential::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use client_builder_impl::*; @@ -11,6 +12,7 @@ pub use confidential_client_application::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; +pub use force_token_refresh::*; pub use open_id_authorization_url::*; pub use open_id_credential::*; pub use prompt::*; @@ -33,6 +35,7 @@ mod app_config; mod application_builder; mod as_query; mod auth_code_authorization_url; +mod authorization_code_assertion_credential; mod authorization_code_certificate_credential; mod authorization_code_credential; mod client_assertion_credential; @@ -43,6 +46,7 @@ mod confidential_client_application; mod device_code_credential; mod display; mod environment_credential; +mod force_token_refresh; mod open_id_authorization_url; mod open_id_credential; mod prompt; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 34304c12..cc6f7e90 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -7,7 +7,7 @@ use url::Url; use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult, AF}; -use graph_extensions::crypto::{secure_random_32, GenPkce}; +use graph_extensions::crypto::secure_random_32; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -51,6 +51,12 @@ pub struct OpenIdAuthorizationUrl { /// To mitigate token replay attacks, your app should verify the nonce value in the ID token /// is the same value it sent when requesting the token. The value is typically a unique, /// random string. + /// + /// Because openid requires a nonce as part of the OAuth flow a nonce is already included. + /// The nonce is generated internally using the same requirements of generating a secure + /// random string as is done when using proof key for code exchange (PKCE) in the + /// authorization code grant. If you are unsure or unclear how the nonce works then it is + /// recommended to stay with the generated nonce as it is cryptographically secure. pub(crate) nonce: String, /// Required - /// A value included in the request that also will be returned in the token response. @@ -404,23 +410,6 @@ impl OpenIdAuthorizationUrlBuilder { self } - /// A value included in the request, generated by the app, that is included in the - /// resulting id_token as a claim. The app can then verify this value to mitigate token - /// replay attacks. The value is typically a randomized, unique string that can be used - /// to identify the origin of the request. - /// - /// The nonce is generated in the same way as generating a PKCE. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - #[doc(hidden)] - pub(crate) fn with_nonce_generated(&mut self) -> anyhow::Result<&mut Self> { - self.parameters.nonce = secure_random_32()?; - Ok(self) - } - pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { self.parameters.state = Some(state.as_ref().to_owned()); self diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 418267b9..b5873762 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -13,11 +13,14 @@ use graph_extensions::crypto::{GenPkce, ProofKeyCodeExchange}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, OpenIdAuthorizationUrl, TokenCredentialExecutor, + Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, + OpenIdAuthorizationUrl, OpenIdAuthorizationUrlBuilder, TokenCredentialExecutor, }; -use crate::oauth::{ConfidentialClientApplication, OpenIdAuthorizationUrlBuilder}; -credential_builder!(OpenIdCredentialBuilder, ConfidentialClientApplication); +credential_builder!( + OpenIdCredentialBuilder, + ConfidentialClientApplication<OpenIdCredential> +); /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your @@ -62,7 +65,6 @@ pub struct OpenIdCredential { pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: OAuthSerializer, } - impl Debug for OpenIdCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("OpenIdCredential") diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 5326733e..dbe1605a 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -1,182 +1,66 @@ -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; - -use async_trait::async_trait; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use reqwest::tls::Version; -use reqwest::{ClientBuilder, Response}; -use url::Url; -use uuid::Uuid; - -use graph_error::{AuthExecutionResult, IdentityResult}; -use graph_extensions::token::ClientApplication; - use crate::identity::credentials::app_config::AppConfig; use crate::identity::credentials::application_builder::PublicClientApplicationBuilder; use crate::identity::{ Authority, AzureCloudInstance, DeviceCodeCredential, ResourceOwnerPasswordCredential, TokenCredentialExecutor, }; +use async_trait::async_trait; +use graph_error::{AuthExecutionResult, IdentityResult}; +use graph_extensions::cache::{AsBearer, TokenCacheStore}; +use graph_extensions::token::ClientApplication; +use reqwest::Response; +use std::collections::HashMap; +use std::fmt::Debug; +use url::Url; +use uuid::Uuid; /// Clients incapable of maintaining the confidentiality of their credentials /// (e.g., clients executing on the device used by the resource owner, such as an /// installed native application or a web browser-based application), and incapable of /// secure client authentication via any other means. /// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 -#[derive(Clone)] -pub struct PublicClientApplication { - http_client: reqwest::Client, - credential: Box<dyn TokenCredentialExecutor + Send>, - //token_store: Arc<RwLock<dyn TokenStore + Send>>, +#[derive(Clone, Debug)] +pub struct PublicClientApplication<Credential> { + credential: Credential, } -impl Debug for PublicClientApplication { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ConfidentialClientApplication") - .field("credential", &self.credential) - .finish() +impl PublicClientApplication<()> { + pub fn builder(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { + PublicClientApplicationBuilder::new(client_id) } } -impl PublicClientApplication { - pub fn new<T>(credential: T) -> PublicClientApplication - where - T: Into<PublicClientApplication>, - { - credential.into() - } - - pub(crate) fn credential<T>(credential: T) -> PublicClientApplication - where - T: TokenCredentialExecutor + Send + 'static, - { - PublicClientApplication { - http_client: ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build() - .unwrap(), - credential: Box::new(credential), - //token_store: Arc::new(RwLock::new(UnInitializedTokenStore)), - } +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> + PublicClientApplication<Credential> +{ + pub(crate) fn new(credential: Credential) -> PublicClientApplication<Credential> { + PublicClientApplication { credential } } - pub fn builder(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { - PublicClientApplicationBuilder::new(client_id.as_ref()) - } - - /* - pub fn with_in_memory_token_store(&mut self) { - self.token_store = Arc::new(RwLock::new(InMemoryCredentialStore::new( - self.app_config().cache_id(), - StoredToken::UnInitialized, - ))); + pub(crate) fn credential(credential: Credential) -> PublicClientApplication<Credential> { + PublicClientApplication { credential } } - */ } #[async_trait] -impl ClientApplication for PublicClientApplication { +impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication + for PublicClientApplication<Credential> +{ fn get_token_silent(&mut self) -> AuthExecutionResult<String> { - todo!() + let token = self.credential.get_token_silent()?; + Ok(token.as_bearer()) } async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { - todo!() + let token = self.credential.get_token_silent_async().await?; + Ok(token.as_bearer()) } } -/* -fn get_token_silent(&mut self) -> AuthExecutionResult<String> { - let cache_id = self.app_config().cache_id(); - if self.is_store_and_token_initialized(cache_id.as_str()) { - return Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error getting token from store - please report issue", - ))? - .clone()); - } - - if !self.is_token_store_initialized() { - self.with_in_memory_token_store(); - } - - let response = self.execute()?; - let msal_token: MsalToken = response.json()?; - self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); - Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error initializing token store - please report issue", - ))? - .clone()) - } - async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { - let cache_id = self.app_config().cache_id(); - if self.is_store_and_token_initialized(cache_id.as_str()) { - return Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error getting token from store - please report issue", - ))? - .clone()); - } - - if !self.is_token_store_initialized() { - self.with_in_memory_token_store(); - } - - let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; - self.update_stored_token(cache_id.as_str(), StoredToken::MsalToken(msal_token)); - Ok(self - .get_bearer_token_from_store(cache_id.as_str()) - .ok_or(AF::unknown( - "Unknown error initializing token store - please report issue", - ))? - .clone()) - } - - fn get_stored_application_token(&mut self) -> Option<&StoredToken> { - let cache_id = self.app_config().cache_id(); - if !self.is_store_and_token_initialized(cache_id.as_str()) { - self.get_token_silent().ok()?; - } - - self.token_store.get_stored_token(cache_id.as_str()) - } - */ - -/* -impl TokenStore for PublicClientApplication { - fn token_store_provider(&self) -> TokenStoreProvider { - self.token_store.read().unwrap().token_store_provider() - } - - fn is_stored_token_initialized(&self, id: &str) -> bool { - self.token_store.read().unwrap().is_stored_token_initialized(id) - } - - fn get_stored_token(&self, id: &str) -> Option<&StoredToken> { - self.token_store.read().unwrap().get_stored_token(id) - } - - fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken> { - *self.token_store.write().unwrap().update_stored_token(id, stored_token) - } - - fn get_bearer_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.read().unwrap().get_bearer_token_from_store(id) - } - - fn get_refresh_token_from_store(&self, id: &str) -> Option<&String> { - self.token_store.read().unwrap().get_refresh_token_from_store(id) - } -} -*/ #[async_trait] -impl TokenCredentialExecutor for PublicClientApplication { +impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor + for PublicClientApplication<Credential> +{ fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() } @@ -202,72 +86,23 @@ impl TokenCredentialExecutor for PublicClientApplication { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let uri = self.credential.uri()?; - - let form = self.credential.form_urlencode()?; - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - let basic_auth = self.credential.basic_auth(); - if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client - .post(uri) - .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) - .send()?) - } else { - Ok(http_client.post(uri).headers(headers).form(&form).send()?) - } + self.credential.execute() } async fn execute_async(&mut self) -> AuthExecutionResult<Response> { - let uri = self.credential.uri()?; - - let form = self.credential.form_urlencode()?; - let basic_auth = self.credential.basic_auth(); - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - if let Some((client_identifier, secret)) = basic_auth { - Ok(self - .http_client - .post(uri) - // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 - .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) - .send() - .await?) - } else { - Ok(self - .http_client - .post(uri) - .headers(headers) - .form(&form) - .send() - .await?) - } + self.credential.execute_async().await } } -impl From<ResourceOwnerPasswordCredential> for PublicClientApplication { +impl From<ResourceOwnerPasswordCredential> + for PublicClientApplication<ResourceOwnerPasswordCredential> +{ fn from(value: ResourceOwnerPasswordCredential) -> Self { PublicClientApplication::credential(value) } } -impl From<DeviceCodeCredential> for PublicClientApplication { +impl From<DeviceCodeCredential> for PublicClientApplication<DeviceCodeCredential> { fn from(value: DeviceCodeCredential) -> Self { PublicClientApplication::credential(value) } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 028c4b1c..b6b9e9c0 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -4,16 +4,17 @@ use std::fmt::Debug; use async_trait::async_trait; use dyn_clone::DynClone; use http::header::ACCEPT; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; +use reqwest::header::{HeaderMap, HeaderValue}; use reqwest::tls::Version; use reqwest::ClientBuilder; -use tracing::{debug, Instrument}; +use tracing::debug; use url::Url; use uuid::Uuid; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; +use crate::identity::AuthorizationRequest; use crate::identity::{Authority, AzureCloudInstance}; dyn_clone::clone_trait_object!(TokenCredentialExecutor); @@ -24,6 +25,20 @@ pub trait TokenCredentialExecutor: DynClone + Debug { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; + fn authorization_request_parts(&mut self) -> IdentityResult<AuthorizationRequest> { + let uri = self.uri()?; + let form = self.form_urlencode()?; + let basic_auth = self.basic_auth(); + let extra_headers = self.extra_header_parameters(); + let extra_query_params = self.extra_query_parameters(); + + let mut auth_request = AuthorizationRequest::new(uri, form, basic_auth); + auth_request.with_extra_headers(extra_headers); + auth_request.with_extra_query_parameters(extra_query_params); + + Ok(auth_request) + } + fn client_id(&self) -> &Uuid { &self.app_config().client_id } @@ -100,59 +115,30 @@ pub trait TokenCredentialExecutor: DynClone + Debug { } fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let mut uri = self.uri()?; - let form = self.form_urlencode()?; let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) .build()?; - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - let extra_headers = self.extra_header_parameters(); - if !extra_headers.is_empty() { - if extra_headers.contains_key(ACCEPT) { - panic!("extra header parameters cannot contain header key ACCEPT") - } - - for (header_name, header_value) in extra_headers.iter() { - headers.insert(header_name, header_value.clone()); - } - } - - let extra_query_params = self.extra_query_parameters(); - if !extra_query_params.is_empty() { - for (key, value) in extra_query_params.iter() { - uri.query_pairs_mut() - .append_pair(key.as_ref(), value.as_ref()); - } - } - let basic_auth = self.basic_auth(); + let auth_request = self.authorization_request_parts()?; + let basic_auth = auth_request.basic_auth; if let Some((client_identifier, secret)) = basic_auth { Ok(http_client - .post(uri) + .post(auth_request.uri) .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded) .send()?) } else { - Ok(http_client.post(uri).form(&form).send()?) + Ok(http_client + .post(auth_request.uri) + .form(&auth_request.form_urlencoded) + .send()?) } } - #[tracing::instrument] - async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { - let mut uri = self.uri()?; - let form = self.form_urlencode()?; - let http_client = ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); + /* + let mut headers = HeaderMap::new(); headers.insert( CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"), @@ -177,13 +163,25 @@ pub trait TokenCredentialExecutor: DynClone + Debug { } } - let basic_auth = self.basic_auth(); + */ + + #[tracing::instrument] + async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { + //let mut uri = self.uri()?; + // let form = self.form_urlencode()?; + let http_client = ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + let auth_request = self.authorization_request_parts()?; + let basic_auth = auth_request.basic_auth; if let Some((client_identifier, secret)) = basic_auth { let request_builder = http_client - .post(uri) + .post(auth_request.uri) .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form); + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); debug!( "authorization request constructed; request={:#?}", @@ -193,7 +191,10 @@ pub trait TokenCredentialExecutor: DynClone + Debug { debug!("authorization response received; response={:#?}", response); Ok(response?) } else { - let request_builder = http_client.post(uri).headers(headers).form(&form); + let request_builder = http_client + .post(auth_request.uri) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); debug!( "authorization request constructed; request={:#?}", diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 7831a85a..434ec51c 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; -use anyhow::anyhow; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; +use graph_error::{IdentityResult, AF}; use openssl::error::ErrorStack; use openssl::hash::MessageDigest; use openssl::pkcs12::{ParsedPkcs12_2, Pkcs12}; @@ -13,25 +13,25 @@ use openssl::x509::{X509Ref, X509}; use time::OffsetDateTime; use uuid::Uuid; -fn encode_cert(cert: &X509) -> anyhow::Result<String> { +fn encode_cert(cert: &X509) -> IdentityResult<String> { Ok(format!( "\"{}\"", - URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| AF::x509(err.to_string()))?) )) } -fn encode_cert_ref(cert: &X509Ref) -> anyhow::Result<String> { +fn encode_cert_ref(cert: &X509Ref) -> IdentityResult<String> { Ok(format!( "\"{}\"", - URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| anyhow!(err.to_string()))?) + URL_SAFE_NO_PAD.encode(cert.to_pem().map_err(|err| AF::x509(err.to_string()))?) )) } #[allow(unused)] -fn thumbprint(cert: &X509) -> anyhow::Result<String> { +fn thumbprint(cert: &X509) -> IdentityResult<String> { let digest_bytes = cert .digest(MessageDigest::sha1()) - .map_err(|err| anyhow!(err.to_string()))?; + .map_err(|err| AF::x509(err.to_string()))?; Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) } @@ -91,16 +91,22 @@ impl X509Certificate { client_id: T, pass: T, certificate: X509, - ) -> anyhow::Result<Self> { + ) -> IdentityResult<Self> { let der = encode_cert(&certificate)?; - let parsed_pkcs12 = - Pkcs12::from_der(&URL_SAFE_NO_PAD.decode(der)?)?.parse2(pass.as_ref())?; - - let _ = parsed_pkcs12.cert.as_ref().ok_or(anyhow::Error::msg( + let parsed_pkcs12 = Pkcs12::from_der( + &URL_SAFE_NO_PAD + .decode(der) + .map_err(|err| AF::x509(err.to_string()))?, + ) + .map_err(|err| AF::x509(err.to_string()))? + .parse2(pass.as_ref()) + .map_err(|err| AF::x509(err.to_string()))?; + + let _ = parsed_pkcs12.cert.as_ref().ok_or(AF::x509( "No certificate found after parsing Pkcs12 using pass", ))?; - let private_key = parsed_pkcs12.pkey.as_ref().ok_or(anyhow::Error::msg( + let private_key = parsed_pkcs12.pkey.as_ref().ok_or(AF::x509( "No private key found after parsing Pkcs12 using pass", ))?; @@ -122,16 +128,22 @@ impl X509Certificate { tenant_id: T, pass: T, certificate: X509, - ) -> anyhow::Result<Self> { + ) -> IdentityResult<Self> { let der = encode_cert(&certificate)?; - let parsed_pkcs12 = - Pkcs12::from_der(&URL_SAFE_NO_PAD.decode(der)?)?.parse2(pass.as_ref())?; - - let _ = parsed_pkcs12.cert.as_ref().ok_or(anyhow::Error::msg( + let parsed_pkcs12 = Pkcs12::from_der( + &URL_SAFE_NO_PAD + .decode(der) + .map_err(|err| AF::x509(err.to_string()))?, + ) + .map_err(|err| AF::x509(err.to_string()))? + .parse2(pass.as_ref()) + .map_err(|err| AF::x509(err.to_string()))?; + + let _ = parsed_pkcs12.cert.as_ref().ok_or(AF::x509( "No certificate found after parsing Pkcs12 using pass", ))?; - let private_key = parsed_pkcs12.pkey.as_ref().ok_or(anyhow::Error::msg( + let private_key = parsed_pkcs12.pkey.as_ref().ok_or(AF::x509( "No private key found after parsing Pkcs12 using pass", ))?; @@ -189,11 +201,11 @@ impl X509Certificate { } /// Base64 Url encoded (No Pad) SHA-1 thumbprint of the X.509 certificate's DER encoding. - pub fn get_thumbprint(&self) -> anyhow::Result<String> { + pub fn get_thumbprint(&self) -> IdentityResult<String> { let digest_bytes = self .certificate .digest(MessageDigest::sha1()) - .map_err(|err| anyhow!(err.to_string()))?; + .map_err(|err| AF::x509(err.to_string()))?; Ok(URL_SAFE_NO_PAD.encode(digest_bytes)) } @@ -215,13 +227,13 @@ impl X509Certificate { self.uuid = value; } - fn x5c(&self) -> anyhow::Result<String> { - let parsed_pkcs12 = self.parsed_pkcs12.as_ref().ok_or(anyhow!( - "No certificate found after parsing Pkcs12 using pass" + fn x5c(&self) -> IdentityResult<String> { + let parsed_pkcs12 = self.parsed_pkcs12.as_ref().ok_or(AF::x509( + "No certificate found after parsing Pkcs12 using pass", ))?; - let certificate = parsed_pkcs12.cert.as_ref().ok_or(anyhow!( - "No certificate found after parsing Pkcs12 using pass" + let certificate = parsed_pkcs12.cert.as_ref().ok_or(AF::x509( + "No certificate found after parsing Pkcs12 using pass", ))?; let sig = encode_cert(certificate)?; @@ -230,9 +242,11 @@ impl X509Certificate { let chain = stack .into_iter() .map(encode_cert_ref) - .collect::<anyhow::Result<Vec<String>>>() + .collect::<IdentityResult<Vec<String>>>() .map_err(|err| { - anyhow!("Unable to encode certificates in certificate chain - error {err}") + AF::x509(format!( + "Unable to encode certificates in certificate chain - error {err}" + )) })? .join(","); @@ -242,7 +256,7 @@ impl X509Certificate { } } - fn get_header(&self) -> anyhow::Result<HashMap<String, String>> { + fn get_header(&self) -> IdentityResult<HashMap<String, String>> { let mut header = HashMap::new(); header.insert("x5t".to_owned(), self.get_thumbprint()?); header.insert("alg".to_owned(), "RS256".to_owned()); @@ -256,7 +270,7 @@ impl X509Certificate { Ok(header) } - fn get_claims(&self, tenant_id: Option<String>) -> anyhow::Result<HashMap<String, String>> { + fn get_claims(&self, tenant_id: Option<String>) -> IdentityResult<HashMap<String, String>> { if let Some(claims) = self.claims.as_ref() { if !self.extend_claims { return Ok(claims.clone()); @@ -295,7 +309,7 @@ impl X509Certificate { } /// JWT Header and Payload in the format header.payload - fn base64_token(&self, tenant_id: Option<String>) -> anyhow::Result<String> { + fn base64_token(&self, tenant_id: Option<String>) -> IdentityResult<String> { let header = self.get_header()?; let header = serde_json::to_string(&header)?; let header_base64 = URL_SAFE_NO_PAD.encode(header.as_bytes()); @@ -326,13 +340,22 @@ impl X509Certificate { Ok(signed_client_assertion) */ - pub fn sign(&self) -> anyhow::Result<String> { + pub fn sign(&self) -> IdentityResult<String> { let token = self.base64_token(self.tenant_id.clone())?; - let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; - signer.set_rsa_padding(Padding::PKCS1)?; - signer.update(token.as_str().as_bytes())?; - let signature = URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey) + .map_err(|err| AF::x509(err.to_string()))?; + signer + .set_rsa_padding(Padding::PKCS1) + .map_err(|err| AF::x509(err.to_string()))?; + signer + .update(token.as_str().as_bytes()) + .map_err(|err| AF::x509(err.to_string()))?; + let signature = URL_SAFE_NO_PAD.encode( + signer + .sign_to_vec() + .map_err(|err| AF::x509(err.to_string()))?, + ); Ok(format!("{token}.{signature}")) } @@ -341,13 +364,22 @@ impl X509Certificate { /// /// The signature is a Base64 Url encoded (No Pad) JWT Header and Payload signed with the private key using SHA_256 /// and RSA padding PKCS1 - pub fn sign_with_tenant(&self, tenant_id: Option<String>) -> anyhow::Result<String> { + pub fn sign_with_tenant(&self, tenant_id: Option<String>) -> IdentityResult<String> { let token = self.base64_token(tenant_id)?; - let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey)?; - signer.set_rsa_padding(Padding::PKCS1)?; - signer.update(token.as_str().as_bytes())?; - let signature = URL_SAFE_NO_PAD.encode(signer.sign_to_vec()?); + let mut signer = Signer::new(MessageDigest::sha256(), &self.pkey) + .map_err(|err| AF::x509(err.to_string()))?; + signer + .set_rsa_padding(Padding::PKCS1) + .map_err(|err| AF::x509(err.to_string()))?; + signer + .update(token.as_str().as_bytes()) + .map_err(|err| AF::x509(err.to_string()))?; + let signature = URL_SAFE_NO_PAD.encode( + signer + .sign_to_vec() + .map_err(|err| AF::x509(err.to_string()))?, + ); Ok(format!("{token}.{signature}")) } diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index fc9ce972..aafa1e24 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -8,8 +8,8 @@ pub use allowed_host_validator::*; pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; +pub use authorization_request::*; pub use authorization_serializer::*; -pub use cache::*; pub use credentials::*; pub use device_code::*; pub use token_validator::*; @@ -18,8 +18,8 @@ mod allowed_host_validator; mod application_options; mod authority; mod authorization_query_response; +mod authorization_request; mod authorization_serializer; -mod cache; mod credentials; mod device_code; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 6548b446..d940e1ee 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -51,7 +51,6 @@ //! } //! ``` -#[macro_use] extern crate log; extern crate pretty_env_logger; #[macro_use] @@ -72,13 +71,10 @@ pub mod oauth { crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, }; - pub use crate::auth::GrantSelector; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; pub use crate::discovery::graph_discovery; pub use crate::discovery::jwt_keys; - pub use crate::grants::GrantRequest; - pub use crate::grants::GrantType; pub use crate::identity::*; pub use crate::oauth_error::OAuthError; pub use crate::strum::IntoEnumIterator; diff --git a/src/client/graph.rs b/src/client/graph.rs index 8c0ef9c1..828dd92d 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -67,7 +67,7 @@ use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_error::GraphFailure; use graph_extensions::token::ClientApplication; use graph_http::api_impl::GraphClientConfiguration; -use graph_oauth::identity::{ClientSecretCredential, ConfidentialClient}; +use graph_oauth::identity::{ClientSecretCredential, ConfidentialClientApplication}; use lazy_static::lazy_static; use std::convert::TryFrom; @@ -563,8 +563,8 @@ impl From<GraphClientConfiguration> for Graph { } } -impl From<ConfidentialClient<ClientSecretCredential>> for Graph { - fn from(value: ConfidentialClient<ClientSecretCredential>) -> Self { +impl From<ConfidentialClientApplication<ClientSecretCredential>> for Graph { + fn from(value: ConfidentialClientApplication<ClientSecretCredential>) -> Self { Graph { client: Client::new(value), endpoint: PARSED_GRAPH_URL.clone(), diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs index 926b8d04..8b896db0 100644 --- a/test-tools/src/oauth.rs +++ b/test-tools/src/oauth.rs @@ -1,90 +1,9 @@ use graph_rs_sdk::oauth::*; -use std::borrow::Cow; -use url::Url; #[derive(Debug, Clone, Eq, PartialEq)] pub struct OAuthTestTool; impl OAuthTestTool { - fn match_grant_credential(grant_request: GrantRequest) -> OAuthParameter { - match grant_request { - GrantRequest::Authorization => OAuthParameter::AuthorizationUrl, - GrantRequest::AccessToken => OAuthParameter::TokenUrl, - GrantRequest::RefreshToken => OAuthParameter::RefreshTokenUrl, - } - } - - pub fn oauth_query_uri_test( - oauth: &mut OAuthSerializer, - grant_type: GrantType, - grant_request: GrantRequest, - includes: Vec<OAuthParameter>, - ) { - let mut url = String::new(); - if grant_request.eq(&GrantRequest::AccessToken) { - let mut atu = oauth.get(OAuthParameter::TokenUrl).unwrap(); - if !atu.ends_with('?') { - atu.push('?'); - } - url.push_str(atu.as_str()); - } else if grant_request.eq(&GrantRequest::RefreshToken) { - let mut rtu = oauth.get(OAuthParameter::RefreshTokenUrl).unwrap(); - if !rtu.ends_with('?') { - rtu.push('?'); - } - url.push_str(rtu.as_str()); - } - url.push_str( - oauth - .encode_uri(grant_type, grant_request) - .unwrap() - .as_str(), - ); - let parsed_url = Url::parse(url.as_str()).unwrap(); - let mut cow_cred: Vec<(Cow<str>, Cow<str>)> = Vec::new(); - let mut cow_cred_false: Vec<(Cow<str>, Cow<str>)> = Vec::new(); - let not_includes = OAuthTestTool::credentials_not_including(&includes); - - for oac in OAuthParameter::iter() { - if oauth.contains(oac) && includes.contains(&oac) && !not_includes.contains(&oac) { - if oac.eq(&OAuthParameter::Scope) { - let s = oauth.join_scopes(" "); - cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); - } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { - let s = oauth.get(oac).unwrap(); - cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); - } - } else if oauth.contains(oac) && not_includes.contains(&oac) { - if oac.eq(&OAuthParameter::Scope) { - let s = oauth.join_scopes(" "); - cow_cred.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); - } else if !oac.eq(&OAuthTestTool::match_grant_credential(grant_request)) { - let s = oauth.get(oac).unwrap(); - cow_cred_false.push((Cow::from(oac.alias()), Cow::from(s.to_owned()))); - } - } - } - - let query = parsed_url.query().unwrap(); - let parse = url::form_urlencoded::parse(query.as_bytes()); - - for query in parse { - assert!(cow_cred.contains(&query)); - assert!(!cow_cred_false.contains(&query)); - } - } - - fn credentials_not_including(included: &[OAuthParameter]) -> Vec<OAuthParameter> { - let mut vec = Vec::new(); - for oac in OAuthParameter::iter() { - if !included.contains(&oac) { - vec.push(oac); - } - } - - vec - } - pub fn oauth_contains_credentials(oauth: &mut OAuthSerializer, credentials: &[OAuthParameter]) { for oac in credentials.iter() { assert!(oauth.contains(*oac)); diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 41b32c42..e480a4d8 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - ConfidentialClientApplication, MsalToken, ResourceOwnerPasswordCredential, - TokenCredentialExecutor, + ClientSecretCredential, ConfidentialClientApplication, MsalToken, + ResourceOwnerPasswordCredential, TokenCredentialExecutor, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -124,7 +124,7 @@ impl OAuthTestCredentials { } } - fn client_credentials(self) -> ConfidentialClientApplication { + fn client_credentials(self) -> ConfidentialClientApplication<ClientSecretCredential> { ConfidentialClientApplication::builder(self.client_id.as_str()) .with_client_secret(self.client_secret.as_str()) .with_tenant(self.tenant.as_str()) @@ -178,7 +178,7 @@ impl OAuthTestClient { pub fn get_client_credentials( &self, creds: OAuthTestCredentials, - ) -> ConfidentialClientApplication { + ) -> ConfidentialClientApplication<ClientSecretCredential> { creds.client_credentials() } @@ -214,8 +214,11 @@ impl OAuthTestClient { pub fn request_access_token(&self) -> Option<(String, MsalToken)> { if Environment::is_local() || Environment::is_travis() { - let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); - self.get_access_token(map.get(self).unwrap()) + let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_access_token(test_client_map.get(self).unwrap()) } else if Environment::is_appveyor() { self.get_access_token(OAuthTestCredentials::new_env()) } else if Environment::is_github() { @@ -229,8 +232,12 @@ impl OAuthTestClient { pub async fn request_access_token_async(&self) -> Option<(String, MsalToken)> { if Environment::is_local() || Environment::is_travis() { - let map: OAuthTestClientMap = OAuthTestClientMap::from_file("./env.json").unwrap(); - self.get_access_token_async(map.get(self).unwrap()).await + let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_access_token_async(test_client_map.get(self).unwrap()) + .await } else if Environment::is_appveyor() { self.get_access_token_async(OAuthTestCredentials::new_env()) .await @@ -269,13 +276,25 @@ impl OAuthTestClient { pub fn client_credentials_by_rid( resource_identity: ResourceIdentity, - ) -> Option<ConfidentialClientApplication> { + ) -> Option<ConfidentialClientApplication<ClientSecretCredential>> { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; Some(test_client.get_client_credentials(credentials)) } + pub fn client_secret_credential_default() -> Option<ClientSecretCredential> { + let app_registration = OAuthTestClient::get_app_registration()?; + let app_registration_client = app_registration.get_default_client_credentials(); + let test_client = app_registration_client + .clients + .get(&OAuthTestClient::ClientCredentials) + .cloned() + .unwrap(); + let confidential_client = test_client.client_credentials(); + Some(confidential_client.into_inner()) + } + pub async fn graph_by_rid_async( resource_identity: ResourceIdentity, ) -> Option<(String, Graph)> { @@ -418,6 +437,8 @@ impl AppRegistrationClient { pub trait GetAppRegistration { fn get_by_resource_identity(&self, value: ResourceIdentity) -> Option<AppRegistrationClient>; fn get_by_str(&self, value: &str) -> Option<AppRegistrationClient>; + + fn get_default_client_credentials(&self) -> AppRegistrationClient; } #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, AsFile, FromFile)] @@ -443,4 +464,13 @@ impl GetAppRegistration for AppRegistrationMap { fn get_by_str(&self, value: &str) -> Option<AppRegistrationClient> { self.apps.get(value).cloned() } + + fn get_default_client_credentials(&self) -> AppRegistrationClient { + let app_registration = self + .apps + .get("graph-rs-default-client-credentials") + .cloned() + .unwrap(); + app_registration + } } diff --git a/tests/grants_code_flow.rs b/tests/grants_code_flow.rs deleted file mode 100644 index 48ff2e08..00000000 --- a/tests/grants_code_flow.rs +++ /dev/null @@ -1,133 +0,0 @@ -use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, MsalToken, OAuthSerializer}; - -#[test] -fn sign_in_code_url() { - // Test the sign in url with a manually set response type. - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .response_type("code") - .add_scope("https://graph.microsoft.com/.default"); - let u = oauth - .encode_uri(GrantType::CodeFlow, GrantRequest::Authorization) - .unwrap(); - - let s = - "https://login.live.com/oauth20_authorize.srf?client_id=bb301aaa-1201-4259-a230923fds32&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&response_type=code&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default".to_string(); - assert_eq!(u, s); -} - -#[test] -fn sign_in_code_url_with_state() { - // Test the sign in url with a manually set response type. - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://example.com/oauth2/v2.0/authorize") - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .response_type("code") - .state("state"); - oauth.add_scope("https://graph.microsoft.com/.default"); - let u = oauth - .encode_uri(GrantType::CodeFlow, GrantRequest::Authorization) - .unwrap(); - let s = - "https://example.com/oauth2/v2.0/authorize?client_id=bb301aaa-1201-4259-a230923fds32&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&state=state&response_type=code&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default".to_string(); - assert_eq!(u, s); -} - -#[test] -fn access_token() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .client_secret("CLDIE3F") - .authorization_url("https://www.example.com/token") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - - let mut builder = MsalToken::default(); - builder - .with_token_type("token") - .with_access_token("access_token") - .with_expires_in(3600) - .with_scope(vec!["scope"]); - - let code_body = oauth - .encode_uri(GrantType::CodeFlow, GrantRequest::AccessToken) - .unwrap(); - assert_eq!( - code_body, - "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&response_type=token&grant_type=authorization_code&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL".to_string() - ); -} - -#[test] -fn refresh_token() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .client_secret("CLDIE3F") - .authorization_url("https://www.example.com/token") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - - let mut access_token = MsalToken::new("access_token", 3600, "asfasf", vec!["Read.Write"]); - access_token.with_refresh_token("32LKLASDKJ"); - oauth.access_token(access_token); - - let body = oauth - .encode_uri(GrantType::CodeFlow, GrantRequest::RefreshToken) - .unwrap(); - assert_eq!( - body, - "client_id=bb301aaa-1201-4259-a230923fds32&client_secret=CLDIE3F&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&grant_type=refresh_token&code=ALDSKFJLKERLKJALSDKJF2209LAKJGFL&refresh_token=32LKLASDKJ".to_string() - ); -} - -#[test] -fn get_refresh_token() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .client_secret("CLDIE3F") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .refresh_token_url("https://www.example.com/token?") - .authorization_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize?") - .token_uri("https://login.microsoftonline.com/common/oauth2/v2.0/token?"); - - let mut access_token = MsalToken::new("Bearer", 3600, "token", vec!["User.Read"]); - access_token.with_refresh_token("32LKLASDKJ"); - oauth.access_token(access_token); - - assert_eq!("32LKLASDKJ", oauth.get_refresh_token().unwrap()); -} - -#[test] -fn multi_scope() { - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .add_scope("Files.Read") - .add_scope("Files.ReadWrite") - .add_scope("Files.Read.All") - .add_scope("Files.ReadWrite.All") - .add_scope("wl.offline_access") - .redirect_uri("http://localhost:8000/redirect") - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .token_uri("https://login.live.com/oauth20_token.srf") - .refresh_token_url("https://login.live.com/oauth20_token.srf") - .response_type("code") - .logout_url("https://login.live.com/oauth20_logout.srf?"); - - let url = oauth - .encode_uri(GrantType::CodeFlow, GrantRequest::Authorization) - .unwrap(); - let test_url = - "https://login.live.com/oauth20_authorize.srf?client_id=bb301aaa-1201-4259-a230923fds32&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fredirect&response_type=code&scope=Files.Read+Files.Read.All+Files.ReadWrite+Files.ReadWrite.All+wl.offline_access"; - assert_eq!(test_url, url.as_str()) -} diff --git a/tests/grants_implicit.rs b/tests/grants_implicit.rs deleted file mode 100644 index 4f4a660d..00000000 --- a/tests/grants_implicit.rs +++ /dev/null @@ -1,19 +0,0 @@ -use graph_rs_sdk::oauth::{GrantRequest, GrantType, OAuthSerializer}; - -#[test] -pub fn implicit_grant_url() { - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .client_id("bb301aaa-1201-4259-a230923fds32") - .add_scope("Read") - .add_scope("Read.Write") - .redirect_uri("http://localhost:8888/redirect") - .response_type("code"); - let url = oauth - .encode_uri(GrantType::Implicit, GrantRequest::Authorization) - .unwrap(); - let test_url = - "https://login.live.com/oauth20_authorize.srf?client_id=bb301aaa-1201-4259-a230923fds32&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&scope=Read+Read.Write&response_type=code"; - assert_eq!(test_url, url); -} diff --git a/tests/grants_openid.rs b/tests/grants_openid.rs deleted file mode 100644 index 04c7e12b..00000000 --- a/tests/grants_openid.rs +++ /dev/null @@ -1,49 +0,0 @@ -use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, OAuthSerializer}; -use url::{Host, Url}; - -pub fn oauth() -> OAuthSerializer { - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.microsoftonline.com/common/oauth2/authorize") - .client_id("6731de76-14a6-49ae-97bc-6eba6914391e") - .response_type("id_token") - .redirect_uri("http://localhost:8080") - .response_mode("form_post") - .add_scope("openid") - .state("12345") - .nonce("7362CAEA-9CA5-4B43-9BA3-34D7C303EBA7"); - - oauth -} - -#[test] -pub fn test_open_id_url() { - let mut oauth = oauth(); - - let url = oauth - .encode_uri(GrantType::OpenId, GrantRequest::Authorization) - .unwrap(); - let test_url = - "https://login.microsoftonline.com/common/oauth2/authorize?client_id=6731de76-14a6-49ae-97bc-6eba6914391e&response_type=id_token&redirect_uri=http%3A%2F%2Flocalhost%3A8080&response_mode=form_post&scope=openid&state=12345&nonce=7362CAEA-9CA5-4B43-9BA3-34D7C303EBA7"; - let parsed_url = Url::parse(url.as_str()).unwrap(); - - assert_eq!("https", parsed_url.scheme()); - assert_eq!( - parsed_url.host(), - Some(Host::Domain("login.microsoftonline.com")) - ); - assert_eq!(test_url, url); -} - -#[test] -pub fn test_access_token_uri() { - let mut oauth = oauth(); - oauth.response_type("id_token code"); - let url_access_token = oauth - .encode_uri(GrantType::OpenId, GrantRequest::AccessToken) - .unwrap(); - let test_url_access_token = - "client_id=6731de76-14a6-49ae-97bc-6eba6914391e&redirect_uri=http%3A%2F%2Flocalhost%3A8080&grant_type=authorization_code&scope=openid"; - assert_eq!(test_url_access_token, url_access_token); -} diff --git a/tests/grants_token_flow.rs b/tests/grants_token_flow.rs deleted file mode 100644 index 6f25bf6f..00000000 --- a/tests/grants_token_flow.rs +++ /dev/null @@ -1,20 +0,0 @@ -use graph_oauth::oauth::GrantType; -use graph_rs_sdk::oauth::{GrantRequest, OAuthSerializer}; - -#[test] -pub fn token_flow_url() { - let mut oauth = OAuthSerializer::new(); - oauth - .authorization_url("https://login.live.com/oauth20_authorize.srf?") - .client_id("bb301aaa-1201-4259-a230923fds32") - .add_scope("Read") - .add_scope("Read.Write") - .redirect_uri("http://localhost:8888/redirect") - .response_type("token"); - let url = oauth - .encode_uri(GrantType::TokenFlow, GrantRequest::Authorization) - .unwrap(); - let test_url = - "https://login.live.com/oauth20_authorize.srf?client_id=bb301aaa-1201-4259-a230923fds32&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Fredirect&response_type=token&scope=Read+Read.Write"; - assert_eq!(test_url, url); -} diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index af72ccdc..815af57f 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -45,7 +45,6 @@ async fn mail_folder_list_messages() { assert!(response.status().is_success()); let body: serde_json::Value = response.json().await.unwrap(); - dbg!(&body); let messages = body["value"].as_array().unwrap(); assert_eq!(messages.len(), 2); } diff --git a/tests/token_cache_tests.rs b/tests/token_cache_tests.rs new file mode 100644 index 00000000..f65683a0 --- /dev/null +++ b/tests/token_cache_tests.rs @@ -0,0 +1,35 @@ +use graph_extensions::cache::TokenCacheStore; +use std::thread; +use std::time::Duration; +use test_tools::oauth_request::OAuthTestClient; + +#[test] +fn token_cache_clone() { + if let Some(mut credential) = OAuthTestClient::client_secret_credential_default() { + let token = credential.get_token_silent().unwrap(); + + thread::sleep(Duration::from_secs(5)); + + let mut credential2 = credential.clone(); + + let token2 = credential2.get_token_silent().unwrap(); + + assert_eq!(token, token2); + } +} + +#[tokio::test] +async fn token_cache_clone_async() { + std::env::set_var("GRAPH_TEST_ENV", "true"); + if let Some(mut credential) = OAuthTestClient::client_secret_credential_default() { + let token = credential.get_token_silent_async().await.unwrap(); + + tokio::time::sleep(Duration::from_secs(5)).await; + + let mut credential2 = credential.clone(); + + let token2 = credential2.get_token_silent_async().await.unwrap(); + + assert_eq!(token, token2); + } +} diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 49eaaae1..2e13cc74 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -64,6 +64,7 @@ fn get_file_content( #[test] fn upload_reqwest_body() { + std::env::set_var("GRAPH_TEST_ENV", "true"); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph() { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; From 7fac99b1e8f6ba12c6fd6d960250487bc5ef5ed6 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:45:18 -0400 Subject: [PATCH 045/118] Put wry crate and interactive auth behind feature --- graph-core/Cargo.toml | 22 +- graph-core/src/http/mod.rs | 3 + .../src/http/response_builder_ext.rs | 0 graph-core/src/lib.rs | 1 + graph-error/src/authorization_failure.rs | 12 + graph-error/src/lib.rs | 2 + graph-extensions/Cargo.toml | 3 - graph-extensions/src/cache/mod.rs | 48 ---- graph-extensions/src/cache/token_store.rs | 68 ------ .../src/cache/token_store_providers.rs | 5 - .../src/cache/token_watch_task.rs | 21 -- graph-extensions/src/http/mod.rs | 4 +- .../src/http/response_builder_ext.rs | 53 +++++ graph-extensions/src/lib.rs | 4 - .../src/web/interactive_authenticator.rs | 31 --- .../src/web/interactive_web_view.rs | 146 ------------ graph-oauth/Cargo.toml | 7 +- .../credentials/application_builder.rs | 39 +--- .../auth_code_authorization_url.rs | 105 +++++---- graph-oauth/src/identity/credentials/mod.rs | 1 + graph-oauth/src/lib.rs | 9 +- .../src/web/interactive_authenticator.rs | 24 ++ graph-oauth/src/web/interactive_web_view.rs | 211 ++++++++++++++++++ .../src/web/mod.rs | 0 .../src/web/web_view_options.rs | 10 +- 25 files changed, 408 insertions(+), 421 deletions(-) create mode 100644 graph-core/src/http/mod.rs rename graph-extensions/src/http/http_ext.rs => graph-core/src/http/response_builder_ext.rs (100%) delete mode 100644 graph-extensions/src/cache/token_store.rs delete mode 100644 graph-extensions/src/cache/token_store_providers.rs delete mode 100644 graph-extensions/src/cache/token_watch_task.rs create mode 100644 graph-extensions/src/http/response_builder_ext.rs delete mode 100644 graph-extensions/src/web/interactive_authenticator.rs delete mode 100644 graph-extensions/src/web/interactive_web_view.rs create mode 100644 graph-oauth/src/web/interactive_authenticator.rs create mode 100644 graph-oauth/src/web/interactive_web_view.rs rename {graph-extensions => graph-oauth}/src/web/mod.rs (100%) rename {graph-extensions => graph-oauth}/src/web/web_view_options.rs (66%) diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index c3ac90bd..d1423fca 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -5,11 +5,29 @@ authors = ["sreeise"] edition = "2021" license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" -description = "Common types for the graph-rs-sdk crate" +description = "Common types and traits for the graph-rs-sdk crate" [dependencies] +async-stream = "0.3" +async-trait = "0.1.35" +dyn-clone = "1.0.14" Inflector = "0.11.4" +http = "0.2.9" +percent-encoding = "2" +reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25.0", features = ["derive"] } remain = "0.2.6" +tracing = "0.1.37" +url = { version = "2", features = ["serde"] } + +graph-error = { path = "../graph-error" } + +[features] +default = ["native-tls"] +native-tls = ["reqwest/native-tls"] +rustls-tls = ["reqwest/rustls-tls"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] diff --git a/graph-core/src/http/mod.rs b/graph-core/src/http/mod.rs new file mode 100644 index 00000000..13626353 --- /dev/null +++ b/graph-core/src/http/mod.rs @@ -0,0 +1,3 @@ +mod response_builder_ext; + +pub use response_builder_ext::*; diff --git a/graph-extensions/src/http/http_ext.rs b/graph-core/src/http/response_builder_ext.rs similarity index 100% rename from graph-extensions/src/http/http_ext.rs rename to graph-core/src/http/response_builder_ext.rs diff --git a/graph-core/src/lib.rs b/graph-core/src/lib.rs index a8c7127c..1dec140c 100644 --- a/graph-core/src/lib.rs +++ b/graph-core/src/lib.rs @@ -6,4 +6,5 @@ extern crate strum; #[macro_use] extern crate serde; +pub mod http; pub mod resource; diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index f96a701f..bcca4c8b 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -120,3 +120,15 @@ pub enum AuthTaskExecutionError<R> { #[error("{0:#?}")] JoinError(#[from] tokio::task::JoinError), } + +#[derive(Debug, thiserror::Error)] +pub enum WebViewExecutionError { + #[error("TimedOut")] + TimedOut, + #[error("InvalidNavigation")] + InvalidNavigation, + #[error("InvalidRedirectUri")] + InvalidRedirectUri, + #[error("WindowCloseRequested")] + WindowCloseRequested, +} diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index 63a8bea9..b553a768 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -19,3 +19,5 @@ pub type GraphResult<T> = Result<T, GraphFailure>; pub type IdentityResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; + +pub type WebViewResult<T> = Result<T, WebViewExecutionError>; diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml index 28a9c3cb..281e87a7 100644 --- a/graph-extensions/Cargo.toml +++ b/graph-extensions/Cargo.toml @@ -17,8 +17,6 @@ chrono-humanize = "0.2.2" dyn-clone = "1.0.14" futures = "0.3.28" http = "0.2.9" -log = "0.4" -pretty_env_logger = "0.4" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.20" serde-aux = "4.1.2" @@ -28,7 +26,6 @@ serde_urlencoded = "0.7.1" time = { version = "0.3.10", features = ["local-offset", "serde"] } tokio = { version = "1.27.0", features = ["full"] } url = { version = "2", features = ["serde"] } -wry = "0.33.0" graph-error = { path = "../graph-error" } diff --git a/graph-extensions/src/cache/mod.rs b/graph-extensions/src/cache/mod.rs index b33d2b66..a306a091 100644 --- a/graph-extensions/src/cache/mod.rs +++ b/graph-extensions/src/cache/mod.rs @@ -1,53 +1,5 @@ mod cache_store; mod in_memory_credential_store; -mod token_store; -mod token_store_providers; -mod token_watch_task; pub use cache_store::*; pub use in_memory_credential_store::*; -use std::fmt::{Debug, Formatter}; -pub use token_store::*; -pub use token_store_providers::*; -pub use token_watch_task::*; - -#[derive(Clone)] -pub struct UnInitializedTokenStore; - -impl TokenStore for UnInitializedTokenStore { - fn token_store_provider(&self) -> TokenStoreProvider { - TokenStoreProvider::UnInitialized - } - - fn is_stored_token_initialized(&self, _id: &str) -> bool { - false - } - - fn get_stored_token(&self, _id: &str) -> Option<&StoredToken> { - panic!("UnInitializedTokenStore does not store tokens") - } - - fn update_stored_token( - &mut self, - _id: &str, - _stored_token: StoredToken, - ) -> Option<StoredToken> { - panic!("UnInitializedTokenStore does not store tokens") - } - - fn get_bearer_token_from_store(&self, _id: &str) -> Option<&String> { - info!("Using uninitialized token store - empty string returned for bearer token"); - Default::default() - } - - fn get_refresh_token_from_store(&self, _id: &str) -> Option<&String> { - info!("Using uninitialized token store - None returned for refresh token"); - Default::default() - } -} - -impl Debug for UnInitializedTokenStore { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str("UnInitializedTokenStore") - } -} diff --git a/graph-extensions/src/cache/token_store.rs b/graph-extensions/src/cache/token_store.rs deleted file mode 100644 index d810f09a..00000000 --- a/graph-extensions/src/cache/token_store.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::cache::TokenStoreProvider; -use crate::token::MsalToken; -use dyn_clone::DynClone; - -#[derive(Debug, Clone, Eq, PartialEq)] -#[allow(clippy::large_enum_variant)] -pub enum StoredToken { - BearerToken(String), - MsalToken(MsalToken), - BearerAndRefreshToken { bearer: String, refresh: String }, - UnInitialized, -} - -impl StoredToken { - pub fn is_initialized(&self) -> bool { - !self.eq(&StoredToken::UnInitialized) - } - - pub fn enable_pii_logging(&mut self) { - if let StoredToken::MsalToken(token) = self { - token.enable_pii_logging(true); - } - } - - pub fn get_bearer_token(&self) -> Option<&String> { - match self { - StoredToken::BearerToken(bearer) => Some(bearer), - StoredToken::MsalToken(msal_token) => Some(&msal_token.access_token), - StoredToken::BearerAndRefreshToken { bearer, refresh: _ } => Some(bearer), - StoredToken::UnInitialized => None, - } - } - - pub fn get_refresh_token(&self) -> Option<&String> { - match self { - StoredToken::BearerToken(_) => None, - StoredToken::MsalToken(msal_token) => msal_token.refresh_token.as_ref(), - StoredToken::BearerAndRefreshToken { bearer: _, refresh } => Some(refresh), - StoredToken::UnInitialized => None, - } - } -} - -dyn_clone::clone_trait_object!(TokenStore); - -pub trait TokenStore: DynClone { - fn token_store_provider(&self) -> TokenStoreProvider; - - fn is_token_store_initialized(&self) -> bool { - !self - .token_store_provider() - .eq(&TokenStoreProvider::UnInitialized) - } - - fn is_stored_token_initialized(&self, id: &str) -> bool; - - fn is_store_and_token_initialized(&self, id: &str) -> bool { - self.is_token_store_initialized() && self.is_stored_token_initialized(id) - } - - fn get_stored_token(&self, id: &str) -> Option<&StoredToken>; - - fn update_stored_token(&mut self, id: &str, stored_token: StoredToken) -> Option<StoredToken>; - - fn get_bearer_token_from_store(&self, id: &str) -> Option<&String>; - - fn get_refresh_token_from_store(&self, id: &str) -> Option<&String>; -} diff --git a/graph-extensions/src/cache/token_store_providers.rs b/graph-extensions/src/cache/token_store_providers.rs deleted file mode 100644 index 43a4f70d..00000000 --- a/graph-extensions/src/cache/token_store_providers.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum TokenStoreProvider { - UnInitialized, - InMemory, -} diff --git a/graph-extensions/src/cache/token_watch_task.rs b/graph-extensions/src/cache/token_watch_task.rs deleted file mode 100644 index ef128939..00000000 --- a/graph-extensions/src/cache/token_watch_task.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::fmt::Debug; -pub use tokio::sync::watch::{channel, Receiver, Sender}; - -#[derive(Clone)] -pub struct AutomaticTokenRefresh<T> { - rx: Receiver<T>, -} - -impl<T: Clone + Debug + Send + Sync> AutomaticTokenRefresh<T> { - pub fn new(init: T) -> (Sender<T>, AutomaticTokenRefresh<T>) { - let (tx, rx) = channel(init); - - (tx, AutomaticTokenRefresh { rx }) - } - - pub async fn call(&mut self) { - while self.rx.changed().await.is_ok() { - println!("received = {:?}", *self.rx.borrow()); - } - } -} diff --git a/graph-extensions/src/http/mod.rs b/graph-extensions/src/http/mod.rs index 2fe9350e..e323e9a4 100644 --- a/graph-extensions/src/http/mod.rs +++ b/graph-extensions/src/http/mod.rs @@ -1,5 +1,5 @@ -mod http_ext; +mod response_builder_ext; mod response_converter; -pub use http_ext::*; +pub use response_builder_ext::*; pub use response_converter::*; diff --git a/graph-extensions/src/http/response_builder_ext.rs b/graph-extensions/src/http/response_builder_ext.rs new file mode 100644 index 00000000..2c9dadbc --- /dev/null +++ b/graph-extensions/src/http/response_builder_ext.rs @@ -0,0 +1,53 @@ +use url::Url; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtUrl(pub Url); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtSerdeJsonValue(pub serde_json::Value); + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct HttpExtVecU8(pub Vec<u8>); + +/// Extension trait for http::response::Builder objects +/// +/// Allows the user to add a `Url` to the http::Response +pub trait HttpResponseBuilderExt { + /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` + /// to the `http::Response` + fn url(self, url: Url) -> Self; + fn json(self, value: &serde_json::Value) -> Self; +} + +impl HttpResponseBuilderExt for http::response::Builder { + fn url(self, url: Url) -> Self { + self.extension(HttpExtUrl(url)) + } + + fn json(self, value: &serde_json::Value) -> Self { + if let Ok(value) = serde_json::to_vec(value) { + return self.extension(HttpExtVecU8(value)); + } + + self + } +} + +pub trait HttpResponseExt { + fn url(&self) -> Option<Url>; + fn json(&self) -> Option<serde_json::Value>; +} + +impl<T> HttpResponseExt for http::Response<T> { + fn url(&self) -> Option<Url> { + self.extensions() + .get::<HttpExtUrl>() + .map(|url| url.clone().0) + } + + fn json(&self) -> Option<serde_json::Value> { + self.extensions() + .get::<HttpExtVecU8>() + .and_then(|value| serde_json::from_slice(value.0.as_slice()).ok()) + } +} diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs index 95decab1..9e267713 100644 --- a/graph-extensions/src/lib.rs +++ b/graph-extensions/src/lib.rs @@ -1,11 +1,7 @@ #[macro_use] extern crate serde; -#[macro_use] -extern crate log; -extern crate pretty_env_logger; pub mod cache; pub mod crypto; pub mod http; pub mod token; -pub mod web; diff --git a/graph-extensions/src/web/interactive_authenticator.rs b/graph-extensions/src/web/interactive_authenticator.rs deleted file mode 100644 index bca1e680..00000000 --- a/graph-extensions/src/web/interactive_authenticator.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::web::WebViewOptions; - -pub trait InteractiveAuthenticator { - fn interactive_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> anyhow::Result<Option<String>>; -} - -/* -let url = self.authorization_url()?; - let redirect_url = self.redirect_uri()?; - let web_view_options = interactive_web_view_options.unwrap_or_default(); - let timeout = web_view_options.timeout.clone(); - let (sender, receiver) = std::sync::mpsc::channel(); - - let handle = std::thread::spawn(move || { - match receiver.recv_timeout(timeout) { - Ok(url) => return Ok(url), - Err(e) => Err(e) - } - }); - - InteractiveWebView::interactive_authentication( - &url, - &redirect_url, - web_view_options, - sender - )?; - handle.join().unwrap().map_err(anyhow::Error::from) - */ diff --git a/graph-extensions/src/web/interactive_web_view.rs b/graph-extensions/src/web/interactive_web_view.rs deleted file mode 100644 index b769ad11..00000000 --- a/graph-extensions/src/web/interactive_web_view.rs +++ /dev/null @@ -1,146 +0,0 @@ -use anyhow::Context; -use std::time::Duration; -use url::Url; - -use crate::web::WebViewOptions; -use wry::application::event_loop::EventLoopBuilder; -use wry::application::platform::windows::EventLoopBuilderExtWindows; -use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, -}; - -#[derive(Debug, Clone)] -pub enum UserEvents { - CloseWindow, - ReachedRedirectUri(Url), - InvalidNavigationAttempt(Option<Url>), -} - -struct WebViewValidHosts { - start_uri: Url, - redirect_uri: Url, -} - -impl WebViewValidHosts { - fn new(start_uri: Url, redirect_uri: Url) -> anyhow::Result<WebViewValidHosts> { - if start_uri.host().is_none() || redirect_uri.host().is_none() { - return Err(anyhow::Error::msg( - "authorization url and redirect uri must have valid uri host", - )); - } - - Ok(WebViewValidHosts { - start_uri, - redirect_uri, - }) - } - - fn is_valid_uri(&self, url: &Url) -> bool { - if let Some(host) = url.host() { - self.start_uri.host().eq(&Some(host.clone())) - || self.redirect_uri.host().eq(&Some(host)) - } else { - false - } - } - - fn is_redirect_host(&self, url: &Url) -> bool { - if let Some(host) = url.host() { - self.redirect_uri.host().eq(&Some(host)) - } else { - false - } - } -} - -pub struct InteractiveWebView; - -impl InteractiveWebView { - pub fn interactive_authentication( - uri: Url, - redirect_uri: Url, - options: WebViewOptions, - sender: std::sync::mpsc::Sender<String>, - ) -> anyhow::Result<()> { - let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() - .with_any_thread(true) - .build(); - let proxy = event_loop.create_proxy(); - let sender2 = sender.clone(); - - let validator = WebViewValidHosts::new(uri.clone(), redirect_uri)?; - - let window = WindowBuilder::new() - .with_title("Sign In") - .with_closable(true) - .with_content_protection(true) - .with_minimizable(true) - .with_maximizable(true) - .with_resizable(true) - .with_theme(options.theme) - .build(&event_loop)?; - - let webview = WebViewBuilder::new(window)? - .with_url(uri.as_ref())? - // Disables file drop - .with_file_drop_handler(|_, _| true) - .with_navigation_handler(move |uri| { - if let Ok(url) = Url::parse(uri.as_str()) { - let is_valid_host = validator.is_valid_uri(&url); - let is_redirect = validator.is_redirect_host(&url); - - if is_redirect { - sender2.send(uri.clone()).context("mpsc error").unwrap(); - std::thread::sleep(Duration::from_secs(1)); - let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); - return true; - } - - if !is_valid_host { - let _ = proxy.send_event(UserEvents::CloseWindow); - } - - is_valid_host - } else { - let _ = proxy.send_event(UserEvents::CloseWindow); - false - } - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::NewEvents(StartCause::Init) => info!("Webview runtime started"), - Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - dbg!(&uri); - info!("Matched on redirect uri - closing window: {uri:#?}"); - sender.send(uri.to_string()).unwrap(); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::InvalidNavigationAttempt(url_option)) => { - error!("WebView attempted to navigate to invalid host - closing window for security reasons. Possible url attempted: {url_option:#?}"); - let _ = webview.clear_all_browsing_data(); - *control_flow = ControlFlow::Exit; - - if options.panic_on_invalid_uri_navigation_attempt { - panic!("WebView attempted to navigate to invalid host. Possible url attempted: {url_option:#?}") - } - } - _ => (), - } - }); - } -} diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index aa5202b1..40c8a9fe 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -35,18 +35,16 @@ strum = { version = "0.24.1", features = ["derive"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset"] } webbrowser = "0.8.7" -wry = "0.30.0" +wry = { version = "0.33.1", optional = true} uuid = { version = "1.3.1", features = ["v4", "serde"] } -log = "0.4" -pretty_env_logger = "0.4" tokio = { version = "1.27.0", features = ["full"] } hyper = { version = "1.0.0-rc.3", features = ["full"] } http-body-util = "0.1.0-rc.2" tracing = "0.1.37" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } graph-error = { path = "../graph-error" } graph-extensions = { path = "../graph-extensions" } +graph-core = { path = "../graph-core" } [features] default = ["native-tls"] @@ -56,6 +54,7 @@ brotli = ["reqwest/brotli"] deflate = ["reqwest/deflate"] trust-dns = ["reqwest/trust-dns"] openssl = ["dep:openssl"] +interactive-auth = ["dep:wry"] [[test]] name = "x509_certificate_tests" diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 8b35b91b..88b86537 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,49 +1,22 @@ use crate::identity::{ application_options::ApplicationOptions, credentials::app_config::AppConfig, - credentials::client_assertion_credential::ClientAssertionCredentialBuilder, AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCertificateCredentialBuilder, - AuthorizationCodeCredentialBuilder, AzureCloudInstance, + AuthorizationCodeCredentialBuilder, ClientAssertionCredentialBuilder, ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, - OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredentialBuilder, + OpenIdAuthorizationUrlBuilder, OpenIdCredentialBuilder, PublicClientApplication, + ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, }; -#[cfg(feature = "openssl")] -use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; -use crate::oauth::{OpenIdAuthorizationUrlBuilder, ResourceOwnerPasswordCredential}; use base64::Engine; use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::env::VarError; -use url::Url; use uuid::Uuid; -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum AuthorityHost { - /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). - /// Maps to the instance url string. - AzureCloudInstance(AzureCloudInstance), - Uri(Url), -} - -impl From<AzureCloudInstance> for AuthorityHost { - fn from(value: AzureCloudInstance) -> Self { - AuthorityHost::AzureCloudInstance(value) - } -} - -impl From<Url> for AuthorityHost { - fn from(value: Url) -> Self { - AuthorityHost::Uri(value) - } -} - -impl Default for AuthorityHost { - fn default() -> Self { - AuthorityHost::AzureCloudInstance(AzureCloudInstance::default()) - } -} +#[cfg(feature = "openssl")] +use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; pub struct ConfidentialClientApplicationBuilder { pub(crate) app_config: AppConfig, @@ -118,7 +91,7 @@ impl ConfidentialClientApplicationBuilder { AuthCodeAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) } - pub fn client_credentials_auth_url_builder( + pub fn client_credentials_authorization_url_builder( &mut self, ) -> ClientCredentialsAuthorizationUrlBuilder { ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 755b60c2..bce4ced8 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -8,13 +8,19 @@ use uuid::Uuid; use graph_error::{IdentityResult, AF}; use graph_extensions::crypto::{secure_random_32, ProofKeyCodeExchange}; -use graph_extensions::web::{InteractiveAuthenticator, WebViewOptions}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationQueryResponse, AuthorizationUrl, AzureCloudInstance, Prompt, - ResponseMode, ResponseType, + Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, +}; + +#[cfg(feature = "interactive-auth")] +use crate::identity::AuthorizationQueryResponse; + +#[cfg(feature = "interactive-auth")] +use crate::web::{ + InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, }; /// Get the authorization url required to perform the initial authorization and redirect in the @@ -175,60 +181,67 @@ impl AuthCodeAuthorizationUrlParameters { self.nonce.as_ref() } + #[cfg(feature = "interactive-auth")] pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, ) -> anyhow::Result<AuthorizationQueryResponse> { - let url_string = self - .interactive_authentication(interactive_web_view_options)? - .ok_or(anyhow::Error::msg( - "Unable to get url from redirect in web view".to_string(), - ))?; - dbg!(&url_string); - /* - - - if let Ok(url) = Url::parse(url_string.as_str()) { - dbg!(&url); - - if let Some(query) = url.query() { - let response_query: AuthResponseQuery = serde_urlencoded::from_str(query)?; - } - + let receiver = self.interactive_authentication(interactive_web_view_options)?; + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + while next.is_none() { + next = iter.next(); } - let query: HashMap<String, String> = url.query_pairs().map(|(key, value)| (key.to_string(), value.to_string())) - .collect(); - - let code = query.get("code"); - let id_token = query.get("id_token"); - let access_token = query.get("access_token"); - let state = query.get("state"); - let nonce = query.get("nonce"); - dbg!(&code, &id_token, &access_token, &state, &nonce); - */ - - let url = Url::parse(&url_string)?; - let query = url.query().or(url.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect, url: {url}"), - ))?; - - let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; - Ok(response_query) + return match next { + None => unreachable!(), + Some(auth_event) => { + match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::TimedOut(duration) => { + Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => { + Err(anyhow::anyhow!("CloseRequested")) + } + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) + } + } + } + } + } + }; } } +#[cfg(feature = "interactive-auth")] mod web_view_authenticator { - use graph_extensions::web::{InteractiveAuthenticator, InteractiveWebView, WebViewOptions}; - use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; + use crate::web::{ + InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, + }; + use graph_error::IdentityResult; impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { fn interactive_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, - ) -> anyhow::Result<Option<String>> { + ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { let uri = self.authorization_url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); @@ -238,20 +251,14 @@ mod web_view_authenticator { std::thread::spawn(move || { InteractiveWebView::interactive_authentication( uri, - redirect_uri, + vec![redirect_uri], web_view_options, sender, ) .unwrap(); }); - let mut iter = receiver.try_iter(); - let mut next = iter.next(); - while next.is_none() { - next = iter.next(); - } - - Ok(next) + Ok(receiver) } } } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 32d300f8..6460821e 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -4,6 +4,7 @@ pub use auth_code_authorization_url::*; pub use authorization_code_assertion_credential::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; +pub use client_assertion_credential::*; pub use client_builder_impl::*; pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index d940e1ee..a50daf49 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -51,8 +51,6 @@ //! } //! ``` -extern crate log; -extern crate pretty_env_logger; #[macro_use] extern crate serde; #[macro_use] @@ -66,6 +64,13 @@ mod oauth_error; pub mod identity; +#[cfg(feature = "interactive-auth")] +pub(crate) mod web; + +pub(crate) mod internal { + pub use graph_core::http::*; +} + pub mod oauth { pub use graph_extensions::{ crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs new file mode 100644 index 00000000..2ab5d97d --- /dev/null +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -0,0 +1,24 @@ +use crate::web::WebViewOptions; +use graph_error::IdentityResult; +use url::Url; + +pub trait InteractiveAuthenticator { + fn interactive_authentication( + &self, + interactive_web_view_options: Option<WebViewOptions>, + ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; +} + +#[derive(Clone)] +pub enum WindowCloseReason { + CloseRequested, + InvalidWindowNavigation, +} + +#[derive(Clone)] +pub enum InteractiveAuthEvent { + InvalidRedirectUri(String), + TimedOut(std::time::Duration), + ReachedRedirectUri(Url), + ClosingWindow(WindowCloseReason), +} diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs new file mode 100644 index 00000000..73ed041f --- /dev/null +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -0,0 +1,211 @@ +use anyhow::Context; +use std::time::Duration; +use url::Url; + +use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use wry::application::event_loop::EventLoopBuilder; +use wry::application::platform::windows::EventLoopBuilderExtWindows; +use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }, + webview::WebViewBuilder, +}; + +#[derive(Debug, Clone)] +pub enum UserEvents { + CloseWindow, + ReachedRedirectUri(Url), + InvalidNavigationAttempt(Option<Url>), +} + +struct WebViewValidHosts { + start_uri: Url, + redirect_uris: Vec<Url>, + ports: Vec<usize>, + is_local_host: bool, +} + +impl WebViewValidHosts { + fn new( + start_uri: Url, + redirect_uris: Vec<Url>, + ports: Vec<usize>, + ) -> anyhow::Result<WebViewValidHosts> { + if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { + return Err(anyhow::Error::msg( + "authorization url and redirect uri must have valid uri host", + )); + } + + let is_local_host = redirect_uris + .iter() + .any(|uri| uri.as_str().eq("http://localhost")); + + if is_local_host && !ports.is_empty() { + return Err(anyhow::anyhow!( + "Redirect uri is http://localhost but not ports were specified".to_string() + )); + } + + Ok(WebViewValidHosts { + start_uri, + redirect_uris, + ports, + is_local_host, + }) + } + + fn is_valid_uri(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + if !self.ports.is_empty() + && self + .redirect_uris + .iter() + .any(|uri| uri.as_str().eq("http://localhost")) + { + let hosts: Vec<url::Host> = self + .redirect_uris + .iter() + .map(|port| url::Host::parse(&format!("http://localhost:{}", port)).unwrap()) + .collect(); + + for redirect_uri in self.redirect_uris.iter() { + if let Some(redirect_uri_host) = redirect_uri.host() { + if hosts.iter().any(|host| host.eq(&redirect_uri_host)) { + return true; + } + } + } + } + + self.start_uri.host().eq(&Some(host.clone())) + || self + .redirect_uris + .iter() + .any(|uri| uri.host().eq(&Some(host.clone()))) + } else { + false + } + } + + fn is_redirect_host(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + self.redirect_uris + .iter() + .any(|uri| uri.host().eq(&Some(host.clone()))) + } else { + false + } + } +} + +pub struct InteractiveWebView; + +impl InteractiveWebView { + #[tracing::instrument] + pub fn interactive_authentication( + uri: Url, + redirect_uris: Vec<Url>, + options: WebViewOptions, + sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, + ) -> anyhow::Result<()> { + let is_local_host = redirect_uris + .iter() + .any(|uri| uri.as_str().eq("http://localhost")); + if is_local_host && !options.ports.is_empty() { + sender + .send(InteractiveAuthEvent::InvalidRedirectUri( + "Redirect uri is http://localhost but not ports were specified".to_string(), + )) + .unwrap(); + } + + let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build(); + let proxy = event_loop.create_proxy(); + let sender2 = sender.clone(); + + let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; + + let window = WindowBuilder::new() + .with_title("Sign In") + .with_closable(true) + .with_content_protection(true) + .with_minimizable(true) + .with_maximizable(true) + .with_resizable(true) + .with_theme(options.theme) + .build(&event_loop)?; + + let webview = WebViewBuilder::new(window)? + .with_url(uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + let is_valid_host = validator.is_valid_uri(&url); + let is_redirect = validator.is_redirect_host(&url); + + if is_redirect { + sender2 + .send(InteractiveAuthEvent::ReachedRedirectUri(url.clone())) + .context("mpsc error") + .unwrap(); + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + std::thread::sleep(Duration::from_secs(1)); + let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); + return true; + } + + if !is_valid_host { + let _ = proxy.send_event(UserEvents::InvalidNavigationAttempt(Some(url))); + } + + is_valid_host + } else { + tracing::info!("Unable to navigate WebView - Option<Url> was None"); + let _ = proxy.send_event(UserEvents::CloseWindow); + false + } + }) + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::NewEvents(StartCause::Init) => tracing::info!("Webview runtime started"), + Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + tracing::info!("Window closing without reaching redirect uri"); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { + tracing::info!("Matched on redirect uri: {uri:#?}"); + tracing::info!("Closing window"); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { + tracing::error!("WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); + if options.close_window_on_invalid_uri_navigation { + tracing::error!("Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); + sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap(); + let _ = webview.clear_all_browsing_data(); + // Wait time to avoid deadlock where window closes before + // the channel has received last event. + std::thread::sleep(Duration::from_secs(1)); + *control_flow = ControlFlow::Exit; + } + } + _ => (), + } + }); + } +} diff --git a/graph-extensions/src/web/mod.rs b/graph-oauth/src/web/mod.rs similarity index 100% rename from graph-extensions/src/web/mod.rs rename to graph-oauth/src/web/mod.rs diff --git a/graph-extensions/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs similarity index 66% rename from graph-extensions/src/web/web_view_options.rs rename to graph-oauth/src/web/web_view_options.rs index e7792de1..8e6d9b7e 100644 --- a/graph-extensions/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -1,24 +1,28 @@ use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct WebViewOptions { - pub panic_on_invalid_uri_navigation_attempt: bool, + // Close window if navigation to a uri that does not match one of the + // given redirect uri's. + pub close_window_on_invalid_uri_navigation: bool, pub theme: Option<wry::application::window::Theme>, /// Provide a list of ports to use for interactive authentication. /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. pub ports: Vec<usize>, pub timeout: Duration, + pub clear_browsing_data: bool, } impl Default for WebViewOptions { fn default() -> Self { WebViewOptions { - panic_on_invalid_uri_navigation_attempt: true, + close_window_on_invalid_uri_navigation: true, theme: None, ports: vec![], // 10 Minutes default timeout timeout: Duration::from_secs(10 * 60), + clear_browsing_data: false, } } } From 905627385ee71a5f088cadec87ab71c35acacbf5 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:47:47 -0400 Subject: [PATCH 046/118] Fix missing import --- graph-oauth/src/identity/credentials/application_builder.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 88b86537..4f0f6788 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -385,8 +385,9 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { mod test { use http::header::AUTHORIZATION; use http::HeaderValue; + use url::Url; - use crate::identity::AadAuthorityAudience; + use crate::identity::{AadAuthorityAudience, AzureCloudInstance}; use super::*; From 563950a7b279e903aae4419dfa01c10af92c089f Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 20 Oct 2023 02:40:34 -0400 Subject: [PATCH 047/118] Fix broken tests --- .../client_credentials_admin_consent.rs | 9 +- .../auth_code_grant.rs | 50 ++++ .../client_credentials.rs | 22 ++ examples/oauth_authorization_url/main.rs | 2 + graph-oauth/src/auth.rs | 244 +----------------- .../credentials/application_builder.rs | 8 +- .../client_certificate_credential.rs | 6 +- .../client_credentials_authorization_url.rs | 29 ++- .../credentials/client_secret_credential.rs | 8 +- graph-oauth/src/lib.rs | 6 +- graph-oauth/src/web/interactive_web_view.rs | 25 +- tests/drive_request.rs | 53 ++-- tests/odata_query.rs | 166 ++++++------ tests/reports_request.rs | 3 +- tests/token_cache_tests.rs | 9 - tests/upload_request_blocking.rs | 1 - tests/upload_session_request.rs | 2 +- 17 files changed, 231 insertions(+), 412 deletions(-) create mode 100644 examples/oauth_authorization_url/auth_code_grant.rs create mode 100644 examples/oauth_authorization_url/client_credentials.rs diff --git a/examples/oauth/client_credentials/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/client_credentials_admin_consent.rs index 6b3ecc28..37dfebd7 100644 --- a/examples/oauth/client_credentials/client_credentials_admin_consent.rs +++ b/examples/oauth/client_credentials/client_credentials_admin_consent.rs @@ -21,7 +21,7 @@ // or admin. See examples/client_credentials.rs use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrl; +use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrlParameters; use warp::Filter; // The client_id must be changed before running this example. @@ -30,7 +30,8 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // Paste the URL into a browser and log in to approve the admin consent. fn get_admin_consent_url() -> IdentityResult<url::Url> { - let authorization_credential = ClientCredentialsAuthorizationUrl::new(CLIENT_ID, REDIRECT_URI)?; + let authorization_credential = + ClientCredentialsAuthorizationUrlParameters::new(CLIENT_ID, REDIRECT_URI)?; authorization_credential.url() } @@ -38,12 +39,12 @@ fn get_admin_consent_url() -> IdentityResult<url::Url> { // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { - let authorization_credential = ClientCredentialsAuthorizationUrl::builder(CLIENT_ID) + let url_builder = ClientCredentialsAuthorizationUrlParameters::builder(CLIENT_ID) .with_redirect_uri(REDIRECT_URI)? .with_state("123") .with_tenant("tenant_id") .build(); - authorization_credential.url() + url_builder.url() } // ------------------------------------------------------------------------------------------------- diff --git a/examples/oauth_authorization_url/auth_code_grant.rs b/examples/oauth_authorization_url/auth_code_grant.rs new file mode 100644 index 00000000..b3eff78f --- /dev/null +++ b/examples/oauth_authorization_url/auth_code_grant.rs @@ -0,0 +1,50 @@ +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, + ConfidentialClientApplication, DeviceCodeCredential, GenPkce, MsalToken, ProofKeyCodeExchange, + TokenCredentialExecutor, TokenRequest, +}; + +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static SCOPE: &str = "User.Read"; // or pass more values to vec![] below + +// Authorization Code Grant Auth URL Builder +pub fn auth_code_grant_authorization() { + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) + .url() + .unwrap(); + + // web browser crate opens default browser. + webbrowser::open(url.as_str()).unwrap(); +} + +// Authorization Code Grant PKCE + +// This example shows how to generate a code_challenge and code_verifier +// to perform the authorization code grant flow with proof key for +// code exchange (PKCE) otherwise known as an assertion. +// +// You can also use values of your own for the assertion. +// +// For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +// And the PKCE RFC: https://tools.ietf.org/html/rfc7636 + +// Open the default system web browser to the sign in url for authorization. +// This method uses AuthorizationCodeAuthorizationUrl to build the sign in +// url and query needed to get an authorization code and opens the default system +// web browser to this Url. +fn auth_code_grant_pkce_authorization() { + let pkce = ProofKeyCodeExchange::oneshot().unwrap(); + + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) + .with_pkce(&pkce) + .url() + .unwrap(); + + webbrowser::open(url.as_str()).unwrap(); +} diff --git a/examples/oauth_authorization_url/client_credentials.rs b/examples/oauth_authorization_url/client_credentials.rs new file mode 100644 index 00000000..7d82ac45 --- /dev/null +++ b/examples/oauth_authorization_url/client_credentials.rs @@ -0,0 +1,22 @@ +use graph_rs_sdk::{error::IdentityResult, oauth::ClientCredentialsAuthorizationUrlParameters}; + +// The client_id must be changed before running this example. +static CLIENT_ID: &str = "<CLIENT_ID>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +// Paste the URL into a browser and log in to approve the admin consent. +fn get_admin_consent_url() -> IdentityResult<url::Url> { + let auth_url_parameters = + ClientCredentialsAuthorizationUrlParameters::new(CLIENT_ID, REDIRECT_URI)?; + auth_url_parameters.url() +} + +// Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. +fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { + let url_builder = ClientCredentialsAuthorizationUrlParameters::builder(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI)? + .with_state("123") + .with_tenant("tenant_id") + .build(); + url_builder.url() +} diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index 5619eb86..0d581bc1 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -8,6 +8,8 @@ #[macro_use] extern crate serde; +mod auth_code_grant; +mod client_credentials; mod legacy; mod openid_connect; diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 5bd2fd90..cc8a1ffd 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -11,7 +11,6 @@ use url::Url; use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; use graph_extensions::token::{IdToken, MsalToken}; -use crate::grants::{GrantRequest, GrantType}; use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; use crate::oauth::ResponseType; use crate::oauth_error::OAuthError; @@ -143,7 +142,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// use graph_oauth::oauth::{OAuthSerializer, GrantType}; + /// use graph_oauth::oauth::OAuthSerializer; /// /// let mut oauth = OAuthSerializer::new(); /// ``` @@ -1069,247 +1068,6 @@ impl OAuthSerializer { required_map.extend(optional_map); Ok(required_map) } - - pub fn encode_uri( - &mut self, - grant: GrantType, - request_type: GrantRequest, - ) -> GraphResult<String> { - let mut encoder = Serializer::new(String::new()); - match grant { - GrantType::TokenFlow => - match request_type { - GrantRequest::Authorization => { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - self.form_encode_credentials(GrantType::TokenFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken | GrantRequest::RefreshToken => { - OAuthError::grant_error( - GrantType::TokenFlow, - GrantRequest::AccessToken, - "Grant type does not use request type. Please use OAuth::request_authorization() for browser requests" - ) - } - } - GrantType::CodeFlow => - match request_type { - GrantRequest::Authorization => { - let _ = self.entry_with(OAuthParameter::ResponseType, "code"); - let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); - self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::Authorization), &mut encoder); - - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken => { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::AccessToken), &mut encoder); - Ok(encoder.finish()) - } - GrantRequest::RefreshToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - self.form_encode_credentials(GrantType::CodeFlow.available_credentials(GrantRequest::RefreshToken), &mut encoder); - Ok(encoder.finish()) - } - } - GrantType::AuthorizationCode => - match request_type { - GrantRequest::Authorization => { - let _ = self.entry_with(OAuthParameter::ResponseType, "code"); - let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); - self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken | GrantRequest::RefreshToken => { - if request_type == GrantRequest::AccessToken { - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - } else { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } - self.form_encode_credentials(GrantType::AuthorizationCode.available_credentials(request_type), &mut encoder); - Ok(encoder.finish()) - } - } - GrantType::Implicit => - match request_type { - GrantRequest::Authorization => { - if !self.scopes.is_empty() { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - } - self.form_encode_credentials(GrantType::Implicit.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken | GrantRequest::RefreshToken => { - OAuthError::grant_error( - GrantType::Implicit, - GrantRequest::AccessToken, - "Grant type does not use request type. Please use OAuth::request_authorization() for browser requests" - ) - } - } - GrantType::DeviceCode => - match request_type { - GrantRequest::Authorization => { - self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::Authorization), &mut encoder); - - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "urn:ietf:params:oauth:grant-type:device_code"); - self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); - Ok(encoder.finish()) - } - GrantRequest::RefreshToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - self.form_encode_credentials(GrantType::DeviceCode.available_credentials(GrantRequest::AccessToken), &mut encoder); - Ok(encoder.finish()) - } - } - GrantType::OpenId => - match request_type { - GrantRequest::Authorization => { - self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::Authorization), &mut encoder); - - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::AccessToken), &mut encoder); - Ok(encoder.finish()) - } - GrantRequest::RefreshToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - self.form_encode_credentials(GrantType::OpenId.available_credentials(GrantRequest::RefreshToken), &mut encoder); - Ok(encoder.finish()) - } - } - GrantType::ClientCredentials => - match request_type { - GrantRequest::Authorization => { - self.form_encode_credentials(GrantType::ClientCredentials.available_credentials(GrantRequest::Authorization), &mut encoder); - let mut url = self.get_or_else(OAuthParameter::AuthorizationUrl)?; - if !url.ends_with('?') { - url.push('?'); - } - url.push_str(encoder.finish().as_str()); - Ok(url) - } - GrantRequest::AccessToken | GrantRequest::RefreshToken => { - self.pre_request_check(GrantType::ClientCredentials, request_type); - self.form_encode_credentials(GrantType::ClientCredentials.available_credentials(request_type), &mut encoder); - Ok(encoder.finish()) - } - } - GrantType::ResourceOwnerPasswordCredentials => { - self.pre_request_check(GrantType::ResourceOwnerPasswordCredentials, request_type); - self.form_encode_credentials(GrantType::ResourceOwnerPasswordCredentials.available_credentials(request_type), &mut encoder); - Ok(encoder.finish()) - } - } - } - - fn pre_request_check(&mut self, grant: GrantType, request_type: GrantRequest) { - match grant { - GrantType::TokenFlow => { - if request_type.eq(&GrantRequest::Authorization) { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - } - } - GrantType::CodeFlow => match request_type { - GrantRequest::Authorization => { - let _ = self.entry_with(OAuthParameter::ResponseType, "code"); - let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); - } - GrantRequest::AccessToken => { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - } - GrantRequest::RefreshToken => { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } - }, - GrantType::AuthorizationCode => match request_type { - GrantRequest::Authorization => { - let _ = self.entry_with(OAuthParameter::ResponseType, "code"); - let _ = self.entry_with(OAuthParameter::ResponseMode, "query"); - } - GrantRequest::AccessToken | GrantRequest::RefreshToken => { - if request_type == GrantRequest::AccessToken { - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - } else { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } - } - }, - GrantType::Implicit => { - if request_type.eq(&GrantRequest::Authorization) && !self.scopes.is_empty() { - let _ = self.entry_with(OAuthParameter::ResponseType, "token"); - } - } - GrantType::DeviceCode => { - if request_type.eq(&GrantRequest::AccessToken) { - let _ = self.entry_with( - OAuthParameter::GrantType, - "urn:ietf:params:oauth:grant-type:device_code", - ); - } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } - } - GrantType::OpenId => { - if request_type.eq(&GrantRequest::AccessToken) { - let _ = self.entry_with(OAuthParameter::GrantType, "authorization_code"); - } else if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } - } - GrantType::ClientCredentials => { - if request_type.eq(&GrantRequest::AccessToken) - || request_type.eq(&GrantRequest::RefreshToken) - { - let _ = self.entry_with(OAuthParameter::GrantType, "client_credentials"); - } - } - GrantType::ResourceOwnerPasswordCredentials => { - if request_type.eq(&GrantRequest::RefreshToken) { - let _ = self.entry_with(OAuthParameter::GrantType, "refresh_token"); - } else { - let _ = self.entry_with(OAuthParameter::GrantType, "password"); - } - } - } - } } /// Extend the OAuth credentials. diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 4f0f6788..e42fb21a 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -3,7 +3,7 @@ use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCertificateCredentialBuilder, AuthorizationCodeCredentialBuilder, ClientAssertionCredentialBuilder, - ClientCredentialsAuthorizationUrlBuilder, ClientSecretCredentialBuilder, + ClientCredentialsAuthorizationUrlParameterBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, OpenIdAuthorizationUrlBuilder, OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, @@ -93,8 +93,10 @@ impl ConfidentialClientApplicationBuilder { pub fn client_credentials_authorization_url_builder( &mut self, - ) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) + ) -> ClientCredentialsAuthorizationUrlParameterBuilder { + ClientCredentialsAuthorizationUrlParameterBuilder::new_with_app_config( + self.app_config.clone(), + ) } pub fn openid_authorization_url_builder(&mut self) -> OpenIdAuthorizationUrlBuilder { diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 4c988c5c..acf87c9b 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -15,7 +15,7 @@ use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; use crate::identity::{ - Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlBuilder, + Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, }; @@ -78,8 +78,8 @@ impl ClientCertificateCredential { pub fn authorization_url_builder<T: AsRef<str>>( client_id: T, - ) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new(client_id) + ) -> ClientCredentialsAuthorizationUrlParameterBuilder { + ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 3d8a613b..ed5a0266 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -6,25 +6,24 @@ use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult}; use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance}; +use crate::identity::{credentials::app_config::AppConfig, Authority, AzureCloudInstance}; #[derive(Clone)] -pub struct ClientCredentialsAuthorizationUrl { +pub struct ClientCredentialsAuthorizationUrlParameters { /// The client (application) ID of the service principal pub(crate) app_config: AppConfig, pub(crate) state: Option<String>, } -impl ClientCredentialsAuthorizationUrl { +impl ClientCredentialsAuthorizationUrlParameters { pub fn new<T: AsRef<str>, U: IntoUrl>( client_id: T, redirect_uri: U, - ) -> IdentityResult<ClientCredentialsAuthorizationUrl> { + ) -> IdentityResult<ClientCredentialsAuthorizationUrlParameters> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; - Ok(ClientCredentialsAuthorizationUrl { + Ok(ClientCredentialsAuthorizationUrlParameters { app_config: AppConfig::new_init( Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), Option::<String>::None, @@ -34,8 +33,10 @@ impl ClientCredentialsAuthorizationUrl { }) } - pub fn builder<T: AsRef<str>>(client_id: T) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new(client_id) + pub fn builder<T: AsRef<str>>( + client_id: T, + ) -> ClientCredentialsAuthorizationUrlParameterBuilder { + ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } pub fn url(&self) -> IdentityResult<Url> { @@ -92,14 +93,14 @@ impl ClientCredentialsAuthorizationUrl { } #[derive(Clone)] -pub struct ClientCredentialsAuthorizationUrlBuilder { - parameters: ClientCredentialsAuthorizationUrl, +pub struct ClientCredentialsAuthorizationUrlParameterBuilder { + parameters: ClientCredentialsAuthorizationUrlParameters, } -impl ClientCredentialsAuthorizationUrlBuilder { +impl ClientCredentialsAuthorizationUrlParameterBuilder { pub fn new<T: AsRef<str>>(client_id: T) -> Self { Self { - parameters: ClientCredentialsAuthorizationUrl { + parameters: ClientCredentialsAuthorizationUrlParameters { app_config: AppConfig::new_with_client_id(client_id), state: None, }, @@ -108,7 +109,7 @@ impl ClientCredentialsAuthorizationUrlBuilder { pub fn new_with_app_config(app_config: AppConfig) -> Self { Self { - parameters: ClientCredentialsAuthorizationUrl { + parameters: ClientCredentialsAuthorizationUrlParameters { app_config, state: None, }, @@ -143,7 +144,7 @@ impl ClientCredentialsAuthorizationUrlBuilder { self } - pub fn build(&self) -> ClientCredentialsAuthorizationUrl { + pub fn build(&self) -> ClientCredentialsAuthorizationUrlParameters { self.parameters.clone() } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 4336b80d..32b341b9 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -13,8 +13,8 @@ use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, - ClientCredentialsAuthorizationUrlBuilder, ConfidentialClientApplication, ForceTokenRefresh, - TokenCredentialExecutor, + ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, + ForceTokenRefresh, TokenCredentialExecutor, }; credential_builder!( @@ -90,8 +90,8 @@ impl ClientSecretCredential { pub fn authorization_url_builder<T: AsRef<str>>( client_id: T, - ) -> ClientCredentialsAuthorizationUrlBuilder { - ClientCredentialsAuthorizationUrlBuilder::new(client_id) + ) -> ClientCredentialsAuthorizationUrlParameterBuilder { + ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } } diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index a50daf49..9e291061 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -34,14 +34,14 @@ //! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! //! pub fn authorization_url(client_id: &str) -> IdentityResult<Url> { -//! let auth_url_parameters = ConfidentialClientApplication::builder(client_id) +//! ConfidentialClientApplication::builder(client_id) //! .auth_code_authorization_url_builder() //! .with_redirect_uri("http://localhost:8000/redirect") //! .with_scope(vec!["user.read"]) -//! .build(); +//! .url() //! } //! -//! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication> { +//! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { //! Ok(ConfidentialClientApplication::builder(client_id) //! .with_authorization_code(authorization_code) //! .with_client_secret(client_secret) diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 73ed041f..cf00e505 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -44,7 +44,7 @@ impl WebViewValidHosts { .iter() .any(|uri| uri.as_str().eq("http://localhost")); - if is_local_host && !ports.is_empty() { + if is_local_host && ports.is_empty() { return Err(anyhow::anyhow!( "Redirect uri is http://localhost but not ports were specified".to_string() )); @@ -60,12 +60,7 @@ impl WebViewValidHosts { fn is_valid_uri(&self, url: &Url) -> bool { if let Some(host) = url.host() { - if !self.ports.is_empty() - && self - .redirect_uris - .iter() - .any(|uri| uri.as_str().eq("http://localhost")) - { + if self.is_local_host && !self.ports.is_empty() { let hosts: Vec<url::Host> = self .redirect_uris .iter() @@ -112,25 +107,13 @@ impl InteractiveWebView { options: WebViewOptions, sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, ) -> anyhow::Result<()> { - let is_local_host = redirect_uris - .iter() - .any(|uri| uri.as_str().eq("http://localhost")); - if is_local_host && !options.ports.is_empty() { - sender - .send(InteractiveAuthEvent::InvalidRedirectUri( - "Redirect uri is http://localhost but not ports were specified".to_string(), - )) - .unwrap(); - } - + let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() .with_any_thread(true) .build(); let proxy = event_loop.create_proxy(); let sender2 = sender.clone(); - let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; - let window = WindowBuilder::new() .with_title("Sign In") .with_closable(true) @@ -197,10 +180,10 @@ impl InteractiveWebView { if options.close_window_on_invalid_uri_navigation { tracing::error!("Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap(); - let _ = webview.clear_all_browsing_data(); // Wait time to avoid deadlock where window closes before // the channel has received last event. std::thread::sleep(Duration::from_secs(1)); + let _ = webview.clear_all_browsing_data(); *control_flow = ControlFlow::Exit; } } diff --git a/tests/drive_request.rs b/tests/drive_request.rs index e46f136a..daf3473e 100644 --- a/tests/drive_request.rs +++ b/tests/drive_request.rs @@ -61,7 +61,7 @@ async fn drive_check_in_out() { let response = result.unwrap(); assert!(response.status().is_success()); - std::thread::sleep(Duration::from_secs(2)); + tokio::time::sleep(Duration::from_secs(2)).await; let response = client .drive(id.as_str()) @@ -78,34 +78,51 @@ async fn drive_check_in_out() { } } +async fn update_item_by_path( + drive_id: &str, + path: &str, + item: &serde_json::Value, + client: &Graph, +) -> GraphResult<reqwest::Response> { + client + .drive(drive_id) + .item_by_path(path) + .update_items(item) + .send() + .await +} + #[tokio::test] async fn drive_update() { if Environment::is_local() { let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let req = client - .drive(id.as_str()) - .item_by_path(":/update_test_document.docx:") - .update_items(&serde_json::json!({ + let req = update_item_by_path( + id.as_str(), + ":/update_test_document.docx:", + &serde_json::json!({ "name": "update_test.docx" - })) - .send() - .await; + }), + &client, + ) + .await; if let Ok(response) = req { assert!(response.status().is_success()); let body: serde_json::Value = response.json().await.unwrap(); assert_eq!(body["name"].as_str(), Some("update_test.docx")); - thread::sleep(Duration::from_secs(2)); - let req = client - .drive(id.as_str()) - .item_by_path(":/update_test.docx:") - .update_items(&serde_json::json!({ + tokio::time::sleep(Duration::from_secs(2)).await; + + let req = update_item_by_path( + id.as_str(), + ":/update_test.docx:", + &serde_json::json!({ "name": "update_test_document.docx" - })) - .send() - .await; + }), + &client, + ) + .await; if let Ok(response) = req { assert!(response.status().is_success()); @@ -208,7 +225,7 @@ async fn drive_upload_item() { file.write_all("Test Update File".as_bytes()).unwrap(); file.sync_all().unwrap(); - thread::sleep(Duration::from_secs(2)); + tokio::time::sleep(Duration::from_secs(2)).await; let update_res = update_file(id.as_str(), onedrive_file_path, local_file, &client).await; @@ -223,7 +240,7 @@ async fn drive_upload_item() { panic!("Request Error. Method: update item. Error: {err:#?}"); } - thread::sleep(Duration::from_secs(2)); + tokio::time::sleep(Duration::from_secs(2)).await; let delete_res = delete_file(id.as_str(), item_id, &client).await; diff --git a/tests/odata_query.rs b/tests/odata_query.rs index 35ae3527..be5a180d 100644 --- a/tests/odata_query.rs +++ b/tests/odata_query.rs @@ -67,104 +67,96 @@ fn expand_filter_query() { ); } -#[tokio::test] -async fn filter_query_request_v1() { - if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .users() - .list_user() - .filter(&["startswith(givenName, 'A')"]) - .send() - .await; - - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let users = body["value"].as_array().unwrap(); - let found_user = users.iter().find(|user| { - let name = user["displayName"].as_str().unwrap(); - name.eq("Adele Vance") - }); - - assert!(found_user.is_some()); - } else if let Err(e) = result { - panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); - } - } +async fn filter_request(client: &Graph) -> GraphResult<reqwest::Response> { + client + .users() + .list_user() + .filter(&["startswith(givenName, 'A')"]) + .send() + .await } -#[tokio::test] -async fn filter_query_request_beta() { - if let Some((_id, mut client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .beta() - .users() - .list_user() - .filter(&["startswith(givenName, 'A')"]) - .send() - .await; - - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let users = body["value"].as_array().unwrap(); - let found_user = users.iter().find(|user| { - let name = user["displayName"].as_str().unwrap(); - name.eq("Adele Vance") - }); - - assert!(found_user.is_some()); - } else if let Err(e) = result { - panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); +async fn filter_request_beta(client: &mut Graph) -> GraphResult<reqwest::Response> { + client + .beta() + .users() + .list_user() + .filter(&["startswith(givenName, 'A')"]) + .send() + .await +} + +async fn order_by_request(client: &Graph) -> GraphResult<reqwest::Response> { + client + .users() + .list_user() + .order_by(&["displayName"]) + .send() + .await +} + +async fn order_by_request_beta(client: &mut Graph) -> GraphResult<reqwest::Response> { + client + .beta() + .users() + .list_user() + .order_by(&["displayName"]) + .send() + .await +} + +async fn validate_order_by_request(beta: bool, client: &mut Graph) { + let result = { + if beta { + order_by_request_beta(client).await + } else { + order_by_request(client).await } + }; + + if let Ok(response) = result { + let body: serde_json::Value = response.json().await.unwrap(); + let users = body["value"].as_array().unwrap(); + let found_user = users.iter().find(|user| { + let name = user["displayName"].as_str().unwrap(); + name.eq("Adele Vance") + }); + + assert!(found_user.is_some()); + } else if let Err(e) = result { + panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); } } -#[tokio::test] -async fn order_by_query_request_v1() { - if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .users() - .list_user() - .order_by(&["displayName"]) - .send() - .await; - - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let users = body["value"].as_array().unwrap(); - let found_user = users.iter().find(|user| { - let name = user["displayName"].as_str().unwrap(); - name.eq("Adele Vance") - }); - - assert!(found_user.is_some()); - } else if let Err(e) = result { - panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); +async fn validate_filter_request(beta: bool, client: &mut Graph) { + let result = { + if beta { + filter_request_beta(client).await + } else { + filter_request(client).await } + }; + + if let Ok(response) = result { + let body: serde_json::Value = response.json().await.unwrap(); + let users = body["value"].as_array().unwrap(); + let found_user = users.iter().find(|user| { + let name = user["displayName"].as_str().unwrap(); + name.eq("Adele Vance") + }); + + assert!(found_user.is_some()); + } else if let Err(e) = result { + panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); } } #[tokio::test] -async fn order_by_request_beta() { +async fn filter_query_request_v1() { if let Some((_id, mut client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .beta() - .users() - .list_user() - .order_by(&["displayName"]) - .send() - .await; - - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let users = body["value"].as_array().unwrap(); - let found_user = users.iter().find(|user| { - let name = user["displayName"].as_str().unwrap(); - name.eq("Adele Vance") - }); - - assert!(found_user.is_some()); - } else if let Err(e) = result { - panic!("Request Error. Method: filter_query_request. Error: {e:#?}"); - } + validate_filter_request(false, &mut client).await; + validate_filter_request(true, &mut client).await; + validate_order_by_request(false, &mut client).await; + validate_order_by_request(true, &mut client).await; } } diff --git a/tests/reports_request.rs b/tests/reports_request.rs index 2a87f435..7bbcbc82 100644 --- a/tests/reports_request.rs +++ b/tests/reports_request.rs @@ -57,7 +57,8 @@ async fn get_office_365_user_counts_reports_text() { .await .unwrap(); - assert!(response.status().is_success()); + let status = response.status(); + assert!(status.is_success()); let text = response.text().await.unwrap(); assert!(!text.is_empty()); } diff --git a/tests/token_cache_tests.rs b/tests/token_cache_tests.rs index f65683a0..63d1e26e 100644 --- a/tests/token_cache_tests.rs +++ b/tests/token_cache_tests.rs @@ -7,29 +7,20 @@ use test_tools::oauth_request::OAuthTestClient; fn token_cache_clone() { if let Some(mut credential) = OAuthTestClient::client_secret_credential_default() { let token = credential.get_token_silent().unwrap(); - thread::sleep(Duration::from_secs(5)); - let mut credential2 = credential.clone(); - let token2 = credential2.get_token_silent().unwrap(); - assert_eq!(token, token2); } } #[tokio::test] async fn token_cache_clone_async() { - std::env::set_var("GRAPH_TEST_ENV", "true"); if let Some(mut credential) = OAuthTestClient::client_secret_credential_default() { let token = credential.get_token_silent_async().await.unwrap(); - tokio::time::sleep(Duration::from_secs(5)).await; - let mut credential2 = credential.clone(); - let token2 = credential2.get_token_silent_async().await.unwrap(); - assert_eq!(token, token2); } } diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 2e13cc74..49eaaae1 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -64,7 +64,6 @@ fn get_file_content( #[test] fn upload_reqwest_body() { - std::env::set_var("GRAPH_TEST_ENV", "true"); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph() { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index 032abed9..66cab4b9 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -177,7 +177,7 @@ async fn test_upload_session() { .unwrap(); assert!(response.status().is_success()); - thread::sleep(Duration::from_secs(2)); + tokio::time::sleep(Duration::from_secs(2)).await; // Channel Upload Session let channel_item_id = From 2c8887d0f91d36b0b0fe9f566572c0c4ec81cce1 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 20 Oct 2023 03:25:36 -0400 Subject: [PATCH 048/118] Move token types back to graph-oauth crate --- Cargo.toml | 1 + .../src/identity}/client_application.rs | 6 -- graph-core/src/identity/mod.rs | 3 + graph-core/src/lib.rs | 1 + graph-extensions/src/cache/cache_store.rs | 15 --- graph-extensions/src/lib.rs | 4 - graph-extensions/src/token/mod.rs | 7 -- graph-http/src/blocking/blocking_client.rs | 2 +- graph-http/src/client.rs | 42 ++++---- graph-http/src/lib.rs | 1 + graph-oauth/src/auth.rs | 95 +------------------ .../authorization_code_credential.rs | 3 +- .../credentials/bearer_token_credential.rs | 58 +++++++++++ .../client_assertion_credential.rs | 3 +- .../client_certificate_credential.rs | 3 +- .../credentials/client_secret_credential.rs | 3 +- .../confidential_client_application.rs | 2 +- graph-oauth/src/identity/credentials/mod.rs | 2 + .../credentials/public_client_application.rs | 2 +- .../credentials/token_credential_executor.rs | 31 +----- .../src/identity}/id_token.rs | 0 graph-oauth/src/identity/mod.rs | 4 + .../src/identity}/msal_token.rs | 20 +++- graph-oauth/src/lib.rs | 4 +- src/client/graph.rs | 25 ++--- tests/drive_request.rs | 2 +- tests/upload_session_request.rs | 2 +- 27 files changed, 120 insertions(+), 221 deletions(-) rename {graph-extensions/src/token => graph-core/src/identity}/client_application.rs (68%) create mode 100644 graph-core/src/identity/mod.rs delete mode 100644 graph-extensions/src/token/mod.rs create mode 100644 graph-oauth/src/identity/credentials/bearer_token_credential.rs rename {graph-extensions/src/token => graph-oauth/src/identity}/id_token.rs (100%) rename {graph-extensions/src/token => graph-oauth/src/identity}/msal_token.rs (97%) diff --git a/Cargo.toml b/Cargo.toml index 705a0832..2362cd20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli", "graph-ex deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate", "graph-extensions/deflate"] trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns", "graph-extensions/trust-dns"] openssl = ["graph-oauth/openssl"] +interactive-auth = ["graph-oauth/interactive-auth"] [dev-dependencies] bytes = { version = "1.4.0" } diff --git a/graph-extensions/src/token/client_application.rs b/graph-core/src/identity/client_application.rs similarity index 68% rename from graph-extensions/src/token/client_application.rs rename to graph-core/src/identity/client_application.rs index 4867f8d5..a0eb4193 100644 --- a/graph-extensions/src/token/client_application.rs +++ b/graph-core/src/identity/client_application.rs @@ -2,12 +2,6 @@ use async_trait::async_trait; use dyn_clone::DynClone; use graph_error::AuthExecutionResult; -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub enum ClientApplicationType { - ConfidentialClientApplication, - PublicClientApplication, -} - dyn_clone::clone_trait_object!(ClientApplication); #[async_trait] diff --git a/graph-core/src/identity/mod.rs b/graph-core/src/identity/mod.rs new file mode 100644 index 00000000..6975f88e --- /dev/null +++ b/graph-core/src/identity/mod.rs @@ -0,0 +1,3 @@ +mod client_application; + +pub use client_application::*; diff --git a/graph-core/src/lib.rs b/graph-core/src/lib.rs index 1dec140c..9d423a40 100644 --- a/graph-core/src/lib.rs +++ b/graph-core/src/lib.rs @@ -7,4 +7,5 @@ extern crate strum; extern crate serde; pub mod http; +pub mod identity; pub mod resource; diff --git a/graph-extensions/src/cache/cache_store.rs b/graph-extensions/src/cache/cache_store.rs index 27006504..62208ee7 100644 --- a/graph-extensions/src/cache/cache_store.rs +++ b/graph-extensions/src/cache/cache_store.rs @@ -1,4 +1,3 @@ -use crate::token::MsalToken; use async_trait::async_trait; use graph_error::AuthExecutionError; @@ -6,14 +5,6 @@ pub trait AsBearer<RHS = Self> { fn as_bearer(&self) -> String; } -pub struct BearerToken(String); - -impl AsBearer for BearerToken { - fn as_bearer(&self) -> String { - self.0.clone() - } -} - impl AsBearer for String { fn as_bearer(&self) -> String { self.clone() @@ -26,12 +17,6 @@ impl AsBearer for &str { } } -impl AsBearer for MsalToken { - fn as_bearer(&self) -> String { - self.access_token.to_string() - } -} - #[async_trait] pub trait TokenCacheStore { type Token: AsBearer; diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs index 9e267713..8b890f74 100644 --- a/graph-extensions/src/lib.rs +++ b/graph-extensions/src/lib.rs @@ -1,7 +1,3 @@ -#[macro_use] -extern crate serde; - pub mod cache; pub mod crypto; pub mod http; -pub mod token; diff --git a/graph-extensions/src/token/mod.rs b/graph-extensions/src/token/mod.rs deleted file mode 100644 index 92c53506..00000000 --- a/graph-extensions/src/token/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod client_application; -mod id_token; -mod msal_token; - -pub use client_application::*; -pub use id_token::*; -pub use msal_token::*; diff --git a/graph-http/src/blocking/blocking_client.rs b/graph-http/src/blocking/blocking_client.rs index 5e828515..237967bc 100644 --- a/graph-http/src/blocking/blocking_client.rs +++ b/graph-http/src/blocking/blocking_client.rs @@ -1,5 +1,5 @@ use crate::internal::GraphClientConfiguration; -use graph_extensions::token::ClientApplication; +use graph_core::identity::ClientApplication; use reqwest::header::HeaderMap; use std::env::VarError; use std::ffi::OsStr; diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index dc1ec7ac..d7436cbb 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,10 +1,9 @@ use crate::blocking::BlockingClient; -use async_trait::async_trait; -use graph_error::AuthExecutionResult; +use graph_core::identity::ClientApplication; use graph_extensions::cache::TokenCacheStore; -use graph_extensions::token::ClientApplication; use graph_oauth::identity::{ - ConfidentialClientApplication, PublicClientApplication, TokenCredentialExecutor, + BearerTokenCredential, ConfidentialClientApplication, PublicClientApplication, + TokenCredentialExecutor, }; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; @@ -19,20 +18,6 @@ fn user_agent_header_from_env() -> Option<HeaderValue> { HeaderValue::from_str(header).ok() } -#[derive(Clone)] -pub struct BearerToken(pub String); - -#[async_trait] -impl ClientApplication for BearerToken { - fn get_token_silent(&mut self) -> AuthExecutionResult<String> { - Ok(self.0.clone()) - } - - async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { - Ok(self.0.clone()) - } -} - #[derive(Clone)] struct ClientConfiguration { client_application: Option<Box<dyn ClientApplication>>, @@ -95,7 +80,9 @@ impl GraphClientConfiguration { } pub fn access_token<AT: ToString>(mut self, access_token: AT) -> GraphClientConfiguration { - self.config.client_application = Some(Box::new(BearerToken(access_token.to_string()))); + self.config.client_application = Some(Box::new(BearerTokenCredential::new( + access_token.to_string(), + ))); self } @@ -114,13 +101,16 @@ impl GraphClientConfiguration { self } - /* - pub fn public_client_application(mut self, public_client: PublicClientApplication) -> Self { + pub fn public_client_application< + Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, + >( + mut self, + public_client: PublicClientApplication<Credential>, + ) -> Self { self.config.client_application = Some(Box::new(public_client)); self } - */ pub fn default_headers(mut self, headers: HeaderMap) -> GraphClientConfiguration { for (key, value) in headers.iter() { self.config.headers.insert(key, value.clone()); @@ -209,7 +199,7 @@ impl GraphClientConfiguration { } } else { Client { - client_application: Box::new(BearerToken(Default::default())), + client_application: Box::new(BearerTokenCredential::new(String::default())), inner: builder.build().unwrap(), headers, builder: config, @@ -243,7 +233,7 @@ impl GraphClientConfiguration { } } else { BlockingClient { - client_application: Box::new(BearerToken(Default::default())), + client_application: Box::new(BearerTokenCredential::new(String::default())), inner: builder.build().unwrap(), headers, } @@ -311,8 +301,8 @@ impl Debug for Client { } } -impl From<BearerToken> for Client { - fn from(value: BearerToken) -> Self { +impl From<BearerTokenCredential> for Client { + fn from(value: BearerTokenCredential) -> Self { Client::new(value) } } diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 051cb0f4..3b6ec102 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -40,5 +40,6 @@ pub mod api_impl { pub use crate::resource_identifier::{ResourceConfig, ResourceIdentifier}; pub use crate::traits::{ApiClientImpl, BodyExt, ODataQuery}; pub use crate::upload_session::UploadSession; + pub use graph_core::identity::ClientApplication; pub use graph_error::{GraphFailure, GraphResult}; } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index cc8a1ffd..41c8d3a6 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -9,7 +9,6 @@ use url::form_urlencoded::Serializer; use url::Url; use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; -use graph_extensions::token::{IdToken, MsalToken}; use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; use crate::oauth::ResponseType; @@ -132,7 +131,6 @@ impl AsRef<str> for OAuthParameter { /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct OAuthSerializer { - access_token: Option<MsalToken>, scopes: BTreeSet<String>, credentials: BTreeMap<String, String>, } @@ -148,7 +146,6 @@ impl OAuthSerializer { /// ``` pub fn new() -> OAuthSerializer { OAuthSerializer { - access_token: None, scopes: BTreeSet::new(), credentials: BTreeMap::new(), } @@ -509,27 +506,6 @@ impl OAuthSerializer { self.insert(OAuthParameter::Prompt, value.to_vec().as_query()) } - /// Set id token for open id. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::{OAuthSerializer, IdToken}; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.id_token(IdToken::new("1345", "code", "state", "session_state")); - /// ``` - pub fn id_token(&mut self, value: IdToken) -> &mut OAuthSerializer { - if let Some(code) = value.code { - self.authorization_code(code.as_str()); - } - if let Some(state) = value.state { - let _ = self.entry_with(OAuthParameter::State, state.as_str()); - } - if let Some(session_state) = value.session_state { - self.session_state(session_state.as_str()); - } - self.insert(OAuthParameter::IdToken, value.id_token.as_str()) - } - /// Set the session state. /// /// # Example @@ -894,69 +870,6 @@ impl OAuthSerializer { pub fn clear_scopes(&mut self) { self.scopes.clear(); } - - /// Set the access token. - /// - /// # Example - /// ``` - /// use graph_oauth::oauth::OAuthSerializer; - /// use graph_oauth::oauth::MsalToken; - /// let mut oauth = OAuthSerializer::new(); - /// let access_token = MsalToken::default(); - /// oauth.access_token(access_token); - /// ``` - pub fn access_token(&mut self, ac: MsalToken) { - if let Some(refresh_token) = ac.refresh_token.as_ref() { - self.refresh_token(refresh_token.as_str()); - } - self.access_token.replace(ac); - } - - /// Get the access token. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::MsalToken; - /// # let access_token = MsalToken::default(); - /// # let mut oauth = OAuthSerializer::new(); - /// # oauth.access_token(access_token); - /// let access_token = oauth.get_access_token().unwrap(); - /// println!("{:#?}", access_token); - /// ``` - pub fn get_access_token(&self) -> Option<MsalToken> { - self.access_token.clone() - } - - /// Get the refrsh token. This method returns the current refresh - /// token stored in OAuth and does not make a request for a refresh - /// token. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::MsalToken; - /// # let mut oauth = OAuthSerializer::new(); - /// let mut access_token = MsalToken::default(); - /// access_token.with_refresh_token("refresh_token"); - /// oauth.access_token(access_token); - /// - /// let refresh_token = oauth.get_refresh_token().unwrap(); - /// println!("{:#?}", refresh_token); - /// ``` - pub fn get_refresh_token(&self) -> GraphResult<String> { - if let Some(refresh_token) = self.get(OAuthParameter::RefreshToken) { - return Ok(refresh_token); - } - - match self.get_access_token() { - Some(token) => match token.refresh_token { - Some(t) => Ok(t), - None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), - }, - None => OAuthError::error_from::<String>(OAuthParameter::RefreshToken), - } - } } impl OAuthSerializer { @@ -1035,13 +948,7 @@ impl OAuthSerializer { pub fn params(&mut self, pairs: Vec<OAuthParameter>) -> GraphResult<HashMap<String, String>> { let mut map: HashMap<String, String> = HashMap::new(); for oac in pairs.iter() { - if oac.eq(&OAuthParameter::RefreshToken) { - if let Some(val) = self.get(*oac) { - map.insert(oac.to_string(), val); - } else { - map.insert("refresh_token".into(), self.get_refresh_token()?); - } - } else if oac.alias().eq("scope") && !self.scopes.is_empty() { + if oac.alias().eq("scope") && !self.scopes.is_empty() { map.insert("scope".into(), self.join_scopes(" ")); } else if let Some(val) = self.get(*oac) { map.insert(oac.to_string(), val); diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 8aa543da..6665f0bb 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -10,12 +10,11 @@ use uuid::Uuid; use graph_error::{AuthExecutionError, IdentityResult, AF}; use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; use graph_extensions::crypto::ProofKeyCodeExchange; -use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, + Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, MsalToken, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; diff --git a/graph-oauth/src/identity/credentials/bearer_token_credential.rs b/graph-oauth/src/identity/credentials/bearer_token_credential.rs new file mode 100644 index 00000000..1195c7fd --- /dev/null +++ b/graph-oauth/src/identity/credentials/bearer_token_credential.rs @@ -0,0 +1,58 @@ +use async_trait::async_trait; +use graph_core::identity::ClientApplication; +use graph_error::AuthExecutionResult; +use graph_extensions::cache::AsBearer; + +#[derive(Clone)] +pub struct BearerTokenCredential(String); + +impl BearerTokenCredential { + pub fn new(access_token: impl ToString) -> BearerTokenCredential { + BearerTokenCredential(access_token.to_string()) + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl ToString for BearerTokenCredential { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsBearer for BearerTokenCredential { + fn as_bearer(&self) -> String { + self.0.clone() + } +} + +impl AsRef<str> for BearerTokenCredential { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl From<&str> for BearerTokenCredential { + fn from(value: &str) -> Self { + BearerTokenCredential(value.to_string()) + } +} + +impl From<String> for BearerTokenCredential { + fn from(value: String) -> Self { + BearerTokenCredential(value) + } +} + +#[async_trait] +impl ClientApplication for BearerTokenCredential { + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + Ok(self.0.clone()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + Ok(self.0.clone()) + } +} diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 98f3b280..1b23b03e 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -8,11 +8,10 @@ use uuid::Uuid; use graph_error::{AuthExecutionError, IdentityResult, AF}; use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; -use graph_extensions::token::MsalToken; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ForceTokenRefresh, TokenCredentialExecutor, + Authority, AzureCloudInstance, ForceTokenRefresh, MsalToken, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index acf87c9b..ad7c4fc0 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -8,7 +8,6 @@ use uuid::Uuid; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult, AF}; use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; -use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -16,7 +15,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::X509Certificate; use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, - ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, + ConfidentialClientApplication, ForceTokenRefresh, MsalToken, TokenCredentialExecutor, }; pub(crate) static CLIENT_ASSERTION_TYPE: &str = diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 32b341b9..9feeb457 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -8,13 +8,12 @@ use uuid::Uuid; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; -use graph_extensions::token::MsalToken; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, - ForceTokenRefresh, TokenCredentialExecutor, + ForceTokenRefresh, MsalToken, TokenCredentialExecutor, }; credential_builder!( diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index cb5bddea..ecc630eb 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -7,9 +7,9 @@ use reqwest::Response; use url::Url; use uuid::Uuid; +use graph_core::identity::ClientApplication; use graph_error::{AuthExecutionResult, IdentityResult}; use graph_extensions::cache::{AsBearer, TokenCacheStore}; -use graph_extensions::token::ClientApplication; use crate::identity::{ credentials::app_config::AppConfig, diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 6460821e..06a45721 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -4,6 +4,7 @@ pub use auth_code_authorization_url::*; pub use authorization_code_assertion_credential::*; pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; +pub use bearer_token_credential::*; pub use client_assertion_credential::*; pub use client_builder_impl::*; pub use client_certificate_credential::*; @@ -39,6 +40,7 @@ mod auth_code_authorization_url; mod authorization_code_assertion_credential; mod authorization_code_certificate_credential; mod authorization_code_credential; +mod bearer_token_credential; mod client_assertion_credential; mod client_certificate_credential; mod client_credentials_authorization_url; diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index dbe1605a..d1ce8141 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -5,9 +5,9 @@ use crate::identity::{ TokenCredentialExecutor, }; use async_trait::async_trait; +use graph_core::identity::ClientApplication; use graph_error::{AuthExecutionResult, IdentityResult}; use graph_extensions::cache::{AsBearer, TokenCacheStore}; -use graph_extensions::token::ClientApplication; use reqwest::Response; use std::collections::HashMap; use std::fmt::Debug; diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index b6b9e9c0..22db49ea 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -137,34 +137,6 @@ pub trait TokenCredentialExecutor: DynClone + Debug { } } - /* - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - let extra_headers = self.extra_header_parameters(); - if !extra_headers.is_empty() { - if extra_headers.contains_key(ACCEPT) { - panic!("extra header parameters cannot contain header key ACCEPT") - } - - for (header_name, header_value) in extra_headers.iter() { - headers.insert(header_name, header_value.clone()); - } - } - - let extra_query_params = self.extra_query_parameters(); - if !extra_query_params.is_empty() { - for (key, value) in extra_query_params.iter() { - uri.query_pairs_mut() - .append_pair(key.as_ref(), value.as_ref()); - } - } - - */ - #[tracing::instrument] async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { //let mut uri = self.uri()?; @@ -209,9 +181,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { #[cfg(test)] mod test { - use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; - use super::*; + use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; #[test] fn open_id_configuration_url_authority_tenant_id() { diff --git a/graph-extensions/src/token/id_token.rs b/graph-oauth/src/identity/id_token.rs similarity index 100% rename from graph-extensions/src/token/id_token.rs rename to graph-oauth/src/identity/id_token.rs diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index aafa1e24..22f1833c 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -12,6 +12,8 @@ pub use authorization_request::*; pub use authorization_serializer::*; pub use credentials::*; pub use device_code::*; +pub use id_token::*; +pub use msal_token::*; pub use token_validator::*; mod allowed_host_validator; @@ -23,4 +25,6 @@ mod authorization_serializer; mod credentials; mod device_code; +mod id_token; +mod msal_token; mod token_validator; diff --git a/graph-extensions/src/token/msal_token.rs b/graph-oauth/src/identity/msal_token.rs similarity index 97% rename from graph-extensions/src/token/msal_token.rs rename to graph-oauth/src/identity/msal_token.rs index c50b0542..b1b4dba5 100644 --- a/graph-extensions/src/token/msal_token.rs +++ b/graph-oauth/src/identity/msal_token.rs @@ -6,7 +6,8 @@ use std::collections::HashMap; use std::fmt; use std::ops::{Add, Sub}; -use crate::token::IdToken; +use crate::identity::IdToken; +use graph_extensions::cache::AsBearer; use std::str::FromStr; use time::OffsetDateTime; @@ -372,8 +373,7 @@ impl Default for MsalToken { client_info: None, timestamp: Some(time::OffsetDateTime::now_utc()), expires_on: Some( - time::OffsetDateTime::from_unix_timestamp(0) - .unwrap_or(time::OffsetDateTime::UNIX_EPOCH), + OffsetDateTime::from_unix_timestamp(0).unwrap_or(time::OffsetDateTime::UNIX_EPOCH), ), additional_fields: Default::default(), log_pii: false, @@ -381,6 +381,18 @@ impl Default for MsalToken { } } +impl ToString for MsalToken { + fn to_string(&self) -> String { + self.access_token.to_string() + } +} + +impl AsBearer for MsalToken { + fn as_bearer(&self) -> String { + self.access_token.to_string() + } +} + impl TryFrom<&str> for MsalToken { type Error = GraphFailure; @@ -473,7 +485,7 @@ impl<'de> Deserialize<'de> for MsalToken { { let phantom_access_token: PhantomMsalToken = Deserialize::deserialize(deserializer)?; - let timestamp = time::OffsetDateTime::now_utc(); + let timestamp = OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); Ok(MsalToken { diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 9e291061..8ec21296 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -72,9 +72,7 @@ pub(crate) mod internal { } pub mod oauth { - pub use graph_extensions::{ - crypto::GenPkce, crypto::ProofKeyCodeExchange, token::IdToken, token::MsalToken, - }; + pub use graph_extensions::{crypto::GenPkce, crypto::ProofKeyCodeExchange}; pub use crate::auth::OAuthParameter; pub use crate::auth::OAuthSerializer; diff --git a/src/client/graph.rs b/src/client/graph.rs index 828dd92d..90e887d1 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AllowedHostValidator, HostValidator, MsalToken, OAuthSerializer}; +use crate::oauth::{AllowedHostValidator, HostValidator, MsalToken}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -64,12 +64,10 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; -use graph_error::GraphFailure; -use graph_extensions::token::ClientApplication; use graph_http::api_impl::GraphClientConfiguration; use graph_oauth::identity::{ClientSecretCredential, ConfidentialClientApplication}; +use graph_oauth::oauth::BearerTokenCredential; use lazy_static::lazy_static; -use std::convert::TryFrom; lazy_static! { static ref PARSED_GRAPH_URL: Url = Url::parse(GRAPH_URL).expect("Unable to set v1 endpoint"); @@ -87,7 +85,7 @@ pub struct Graph { impl Graph { pub fn new<AT: ToString>(access_token: AT) -> Graph { Graph { - client: Client::new(BearerToken(access_token.to_string())), + client: Client::new(BearerTokenCredential::new(access_token.to_string())), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), } @@ -526,30 +524,19 @@ impl Graph { impl From<&str> for Graph { fn from(token: &str) -> Self { - Graph::from_client_app(BearerToken(token.into())) + Graph::from_client_app(BearerTokenCredential::new(token)) } } impl From<String> for Graph { fn from(token: String) -> Self { - Graph::from_client_app(BearerToken(token)) + Graph::from_client_app(BearerTokenCredential::new(token)) } } impl From<&MsalToken> for Graph { fn from(token: &MsalToken) -> Self { - Graph::from_client_app(BearerToken(token.access_token.clone())) - } -} - -impl TryFrom<&OAuthSerializer> for Graph { - type Error = GraphFailure; - - fn try_from(oauth: &OAuthSerializer) -> Result<Self, Self::Error> { - let access_token = oauth - .get_access_token() - .ok_or_else(|| GraphFailure::not_found("no access token"))?; - Ok(Graph::from(&access_token)) + Graph::from_client_app(BearerTokenCredential::new(token.access_token.clone())) } } diff --git a/tests/drive_request.rs b/tests/drive_request.rs index daf3473e..15533267 100644 --- a/tests/drive_request.rs +++ b/tests/drive_request.rs @@ -6,7 +6,7 @@ use graph_rs_sdk::{ }; use std::fs::OpenOptions; use std::io::Write; -use std::thread; + use std::time::Duration; use test_tools::oauth_request::DRIVE_ASYNC_THROTTLE_MUTEX; use test_tools::oauth_request::{Environment, OAuthTestClient}; diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index 66cab4b9..f1744249 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -3,7 +3,7 @@ use graph_error::{GraphFailure, GraphResult}; use graph_http::api_impl::UploadSession; use graph_http::traits::ResponseExt; use graph_rs_sdk::Graph; -use std::thread; + use std::time::Duration; use test_tools::oauth_request::{OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX}; From cc245651be529403e918a8434fd1fd32b0777265 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:45:31 -0400 Subject: [PATCH 049/118] Update token cache handling in auth code --- .../auth_code_grant/auth_code_grant_pkce.rs | 4 +- .../auth_code_grant/auth_code_grant_secret.rs | 4 +- examples/oauth/client_credentials/mod.rs | 14 +- examples/oauth/device_code.rs | 9 +- examples/oauth/is_access_token_expired.rs | 14 +- examples/oauth/main.rs | 6 +- .../openid_connect_form_post.rs | 4 +- .../auth_code_grant.rs | 2 +- examples/oauth_authorization_url/main.rs | 2 +- examples/oauth_certificate/main.rs | 4 +- .../src/cache/in_memory_credential_store.rs | 19 +- .../src/identity/authorization_request.rs | 8 +- .../credentials/application_builder.rs | 12 +- .../authorization_code_credential.rs | 138 ++++++++++---- .../client_assertion_credential.rs | 16 +- .../client_certificate_credential.rs | 16 +- .../credentials/client_secret_credential.rs | 22 +-- .../credentials/token_credential_executor.rs | 169 ++++++++++-------- graph-oauth/src/identity/msal_token.rs | 56 +++--- src/client/graph.rs | 6 +- test-tools/src/oauth_request.rs | 69 ++++--- 21 files changed, 363 insertions(+), 231 deletions(-) diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index 822e2a5b..8abfdc53 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -2,7 +2,7 @@ use graph_oauth::identity::ResponseType; use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - GenPkce, MsalToken, ProofKeyCodeExchange, TokenCredentialExecutor, TokenRequest, + GenPkce, ProofKeyCodeExchange, Token, TokenCredentialExecutor, TokenRequest, }; use lazy_static::lazy_static; use warp::{get, Filter}; @@ -68,7 +68,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let access_token: MsalToken = response.json().await.unwrap(); + let access_token: Token = response.json().await.unwrap(); // If all went well here we can print out the OAuth config with the Access Token. println!("AccessToken: {:#?}", access_token.access_token); diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index 24169ff1..e1cd97c6 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::ErrorMessage; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - MsalToken, TokenCredentialExecutor, TokenRequest, + Token, TokenCredentialExecutor, TokenRequest, }; use graph_rs_sdk::*; use warp::Filter; @@ -92,7 +92,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let mut access_token: MsalToken = response.json().await.unwrap(); + let mut access_token: Token = response.json().await.unwrap(); // Enables the printing of the bearer, refresh, and id token. access_token.enable_pii_logging(true); diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index aa65718a..96a5f203 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -10,7 +10,7 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, MsalToken, TokenCredentialExecutor, + ClientSecretCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, TokenRequest, }; @@ -22,12 +22,16 @@ pub use client_credentials_admin_consent::*; // flow after admin consent has been granted. If you have not granted admin consent, see // examples/client_credentials_admin_consent.rs for more info. -// The client_id and client_secret must be changed before running this example. +// Replace client id, client secret, and tenant id with your own values. static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static TENANT_ID: &str = "<TENANT_ID>"; pub async fn get_token_silent() { - let client_secret_credential = ClientSecretCredential::new(CLIENT_ID, CLIENT_SECRET); + let client_secret_credential = ConfidentialClientApplication::builder(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) + .build(); let mut confidential_client_application = ConfidentialClientApplication::from(client_secret_credential); @@ -37,7 +41,7 @@ pub async fn get_token_silent() { .unwrap(); println!("{response:#?}"); - let body: MsalToken = response.json().await.unwrap(); + let body: Token = response.json().await.unwrap(); } pub async fn get_token_silent2() { @@ -48,5 +52,5 @@ pub async fn get_token_silent2() { let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let body: MsalToken = response.json().await.unwrap(); + let body: Token = response.json().await.unwrap(); } diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 17cef1a7..01087ba7 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,8 +1,7 @@ -use graph_oauth::identity::{ - DeviceCodeCredential, PublicClientApplication, TokenCredentialExecutor, +use graph_rs_sdk::oauth::{ + DeviceCodeCredential, DeviceCodeCredentialBuilder, PublicClientApplication, Token, + TokenCredentialExecutor, }; -use graph_oauth::oauth::DeviceCodeCredentialBuilder; -use graph_rs_sdk::oauth::{MsalToken, OAuthSerializer}; use graph_rs_sdk::GraphResult; use std::time::Duration; use warp::hyper::body::HttpBody; @@ -39,7 +38,7 @@ fn get_token(device_code: &str) { let response = public_client.execute().unwrap(); println!("{:#?}", response); - let body: MsalToken = response.json().unwrap(); + let body: Token = response.json().unwrap(); println!("{:#?}", body); } diff --git a/examples/oauth/is_access_token_expired.rs b/examples/oauth/is_access_token_expired.rs index 0ed35625..ef8bc0cf 100644 --- a/examples/oauth/is_access_token_expired.rs +++ b/examples/oauth/is_access_token_expired.rs @@ -1,15 +1,15 @@ -use graph_rs_sdk::oauth::MsalToken; +use graph_rs_sdk::oauth::Token; use std::thread; use std::time::Duration; pub fn is_access_token_expired() { - let mut access_token = MsalToken::default(); - access_token.with_expires_in(1); + let mut token = Token::default(); + token.with_expires_in(1); thread::sleep(Duration::from_secs(3)); - assert!(access_token.is_expired()); + assert!(token.is_expired()); - let mut access_token = MsalToken::default(); - access_token.with_expires_in(10); + let mut token = Token::default(); + token.with_expires_in(10); thread::sleep(Duration::from_secs(4)); - assert!(!access_token.is_expired()); + assert!(!token.is_expired()); } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index a5079741..815e4bfb 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -27,7 +27,7 @@ use graph_extensions::crypto::GenPkce; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, MsalToken, ProofKeyCodeExchange, PublicClientApplication, + DeviceCodeCredential, ProofKeyCodeExchange, PublicClientApplication, Token, TokenCredentialExecutor, TokenRequest, }; @@ -71,7 +71,7 @@ async fn auth_code_grant(authorization_code: &str) { let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: MsalToken = response.json().await.unwrap(); + let access_token: Token = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } @@ -84,6 +84,6 @@ async fn client_credentials() { let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: MsalToken = response.json().await.unwrap(); + let access_token: Token = response.json().await.unwrap(); println!("{:#?}", access_token.access_token); } diff --git a/examples/oauth/openid_connect/openid_connect_form_post.rs b/examples/oauth/openid_connect/openid_connect_form_post.rs index f4861c46..232382d5 100644 --- a/examples/oauth/openid_connect/openid_connect_form_post.rs +++ b/examples/oauth/openid_connect/openid_connect_form_post.rs @@ -3,7 +3,7 @@ use graph_oauth::identity::{ TokenRequest, }; use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; -use graph_rs_sdk::oauth::{IdToken, MsalToken, OAuthSerializer}; +use graph_rs_sdk::oauth::{IdToken, OAuthSerializer, Token}; use tracing_subscriber::fmt::format::FmtSpan; use url::Url; @@ -64,7 +64,7 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let mut response = confidential_client.execute_async().await.unwrap(); if response.status().is_success() { - let mut access_token: MsalToken = response.json().await.unwrap(); + let mut access_token: Token = response.json().await.unwrap(); access_token.enable_pii_logging(true); println!("\n{:#?}\n", access_token); diff --git a/examples/oauth_authorization_url/auth_code_grant.rs b/examples/oauth_authorization_url/auth_code_grant.rs index b3eff78f..48fcc410 100644 --- a/examples/oauth_authorization_url/auth_code_grant.rs +++ b/examples/oauth_authorization_url/auth_code_grant.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::oauth::{ AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, - ConfidentialClientApplication, DeviceCodeCredential, GenPkce, MsalToken, ProofKeyCodeExchange, + ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, Token, TokenCredentialExecutor, TokenRequest, }; diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index 0d581bc1..0e6b7689 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -16,7 +16,7 @@ mod openid_connect; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, GenPkce, MsalToken, ProofKeyCodeExchange, PublicClientApplication, + DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, TokenCredentialExecutor, TokenRequest, }; diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 2b994e3f..23ba1b6e 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,7 +4,7 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AuthorizationCodeCertificateCredential, ConfidentialClientApplication, MsalToken, PKey, + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, Token, TokenCredentialExecutor, X509Certificate, X509, }; use std::fs::File; @@ -118,7 +118,7 @@ async fn handle_redirect( println!("{response:#?}"); if response.status().is_success() { - let mut msal_token: MsalToken = response.json().await.unwrap(); + let mut msal_token: Token = response.json().await.unwrap(); msal_token.enable_pii_logging(true); // If all went well here we can print out the Access Token. diff --git a/graph-extensions/src/cache/in_memory_credential_store.rs b/graph-extensions/src/cache/in_memory_credential_store.rs index 4237bfac..7d69bad7 100644 --- a/graph-extensions/src/cache/in_memory_credential_store.rs +++ b/graph-extensions/src/cache/in_memory_credential_store.rs @@ -3,24 +3,27 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; #[derive(Clone, Default)] -pub struct InMemoryCredentialStore<Token: AsBearer + Clone> { +pub struct InMemoryTokenStore<Token: AsBearer + Clone> { store: Arc<RwLock<HashMap<String, Token>>>, } -impl<Token: AsBearer + Clone> InMemoryCredentialStore<Token> { - pub fn new() -> InMemoryCredentialStore<Token> { - InMemoryCredentialStore { +impl<Token: AsBearer + Clone> InMemoryTokenStore<Token> { + pub fn new() -> InMemoryTokenStore<Token> { + InMemoryTokenStore { store: Default::default(), } } pub fn store<T: Into<String>>(&mut self, cache_id: T, token: Token) { - let mut store = self.store.write().unwrap(); - store.insert(cache_id.into(), token); + let mut write_lock = self.store.write().unwrap(); + write_lock.insert(cache_id.into(), token); + drop(write_lock); } pub fn get(&self, cache_id: &str) -> Option<Token> { - let store = self.store.read().unwrap(); - store.get(cache_id).cloned() + let read_lock = self.store.read().unwrap(); + let token = read_lock.get(cache_id).cloned(); + drop(read_lock); + token } } diff --git a/graph-oauth/src/identity/authorization_request.rs b/graph-oauth/src/identity/authorization_request.rs index 72e154ab..2add7245 100644 --- a/graph-oauth/src/identity/authorization_request.rs +++ b/graph-oauth/src/identity/authorization_request.rs @@ -3,25 +3,25 @@ use http::{HeaderMap, HeaderValue}; use std::collections::HashMap; use url::Url; -pub struct AuthorizationRequest { +pub struct AuthorizationRequestParts { pub(crate) uri: Url, pub(crate) form_urlencoded: HashMap<String, String>, pub(crate) basic_auth: Option<(String, String)>, pub(crate) headers: HeaderMap, } -impl AuthorizationRequest { +impl AuthorizationRequestParts { pub fn new( uri: Url, form_urlencoded: HashMap<String, String>, basic_auth: Option<(String, String)>, - ) -> AuthorizationRequest { + ) -> AuthorizationRequestParts { let mut headers = HeaderMap::new(); headers.insert( CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"), ); - AuthorizationRequest { + AuthorizationRequestParts { uri, form_urlencoded, basic_auth, diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index e42fb21a..5b71a16a 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -1,12 +1,12 @@ use crate::identity::{ application_options::ApplicationOptions, credentials::app_config::AppConfig, AuthCodeAuthorizationUrlParameterBuilder, Authority, - AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCertificateCredentialBuilder, - AuthorizationCodeCredentialBuilder, ClientAssertionCredentialBuilder, - ClientCredentialsAuthorizationUrlParameterBuilder, ClientSecretCredentialBuilder, - DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, - OpenIdAuthorizationUrlBuilder, OpenIdCredentialBuilder, PublicClientApplication, - ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, + AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, + ClientAssertionCredentialBuilder, ClientCredentialsAuthorizationUrlParameterBuilder, + ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, + EnvironmentCredential, OpenIdAuthorizationUrlBuilder, OpenIdCredentialBuilder, + PublicClientApplication, ResourceOwnerPasswordCredential, + ResourceOwnerPasswordCredentialBuilder, }; use base64::Engine; use graph_error::{IdentityResult, AF}; diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 6665f0bb..be62afd0 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -7,14 +7,14 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_error::{AuthExecutionError, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; +use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use graph_extensions::crypto::ProofKeyCodeExchange; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, MsalToken, + Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; @@ -62,7 +62,7 @@ pub struct AuthorizationCodeCredential { /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryCredentialStore<MsalToken>, + token_cache: InMemoryTokenStore<Token>, } impl Debug for AuthorizationCodeCredential { @@ -74,59 +74,115 @@ impl Debug for AuthorizationCodeCredential { } } +impl AuthorizationCodeCredential { + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } +} + #[async_trait] impl TokenCacheStore for AuthorizationCodeCredential { - type Token = MsalToken; + type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); match self.app_config.force_token_refresh { ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + match self.execute_cached_token_refresh(cache_id.clone()) { + Ok(token) => return Ok(token), + Err(_) => {} + } + } + if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: MsalToken = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh(cache_id) } else { - Ok(token) + Ok(token.clone()) } } else { - let response = self.execute()?; - let msal_token: MsalToken = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + self.execute_cached_token_refresh(cache_id) } } ForceTokenRefresh::Once | ForceTokenRefresh::Always => { - let response = self.execute()?; - let msal_token: MsalToken = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); + let token_result = self.execute_cached_token_refresh(cache_id); if self.app_config.force_token_refresh == ForceTokenRefresh::Once { self.app_config.force_token_refresh = ForceTokenRefresh::Never; } - Ok(msal_token) + token_result } } } async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) - } else { - Ok(token.clone()) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + match self + .execute_cached_token_refresh_async(cache_id.clone()) + .await + { + Ok(token) => return Ok(token), + Err(_) => {} + } + } + + if let Some(old_token) = self.token_cache.get(cache_id.as_str()) { + if old_token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = old_token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh_async(cache_id).await + } else { + Ok(old_token.clone()) + } + } else { + self.execute_cached_token_refresh_async(cache_id).await + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh_async(cache_id).await; + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } } @@ -325,7 +381,12 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { } } - if let Some(refresh_token) = self.refresh_token.as_ref() { + let should_attempt_refresh = self.refresh_token.is_some() + && self.app_config.force_token_refresh != ForceTokenRefresh::Once + && self.app_config.force_token_refresh != ForceTokenRefresh::Always; + + if should_attempt_refresh { + let refresh_token = self.refresh_token.clone().unwrap_or_default(); if refresh_token.trim().is_empty() { return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); } @@ -470,4 +531,17 @@ mod test { let map = credential.form_urlencode().unwrap(); assert_eq!(map.get("client_id"), Some(&uuid_value)) } + + #[test] + fn should_force_refresh_test() { + let mut credential_builder = + AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); + let mut credential = credential_builder + .with_redirect_uri("https://localhost") + .unwrap() + .with_client_secret("client_secret") + .with_scope(vec!["scope"]) + .with_tenant("tenant_id") + .build(); + } } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 1b23b03e..e81ddcb5 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -7,11 +7,11 @@ use url::Url; use uuid::Uuid; use graph_error::{AuthExecutionError, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ForceTokenRefresh, MsalToken, TokenCredentialExecutor, + Authority, AzureCloudInstance, ForceTokenRefresh, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; @@ -33,7 +33,7 @@ pub struct ClientAssertionCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryCredentialStore<MsalToken>, + token_cache: InMemoryTokenStore<Token>, } impl ClientAssertionCredential { @@ -65,14 +65,14 @@ impl Debug for ClientAssertionCredential { #[async_trait] impl TokenCacheStore for ClientAssertionCredential { - type Token = MsalToken; + type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { @@ -80,7 +80,7 @@ impl TokenCacheStore for ClientAssertionCredential { } } else { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } @@ -91,7 +91,7 @@ impl TokenCacheStore for ClientAssertionCredential { if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { @@ -99,7 +99,7 @@ impl TokenCacheStore for ClientAssertionCredential { } } else { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index ad7c4fc0..2f64e0dd 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -7,7 +7,7 @@ use url::Url; use uuid::Uuid; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -15,7 +15,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::X509Certificate; use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, - ConfidentialClientApplication, ForceTokenRefresh, MsalToken, TokenCredentialExecutor, + ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, }; pub(crate) static CLIENT_ASSERTION_TYPE: &str = @@ -40,7 +40,7 @@ pub struct ClientCertificateCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryCredentialStore<MsalToken>, + token_cache: InMemoryTokenStore<Token>, } impl ClientCertificateCredential { @@ -93,14 +93,14 @@ impl Debug for ClientCertificateCredential { #[async_trait] impl TokenCacheStore for ClientCertificateCredential { - type Token = MsalToken; + type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { @@ -108,7 +108,7 @@ impl TokenCacheStore for ClientCertificateCredential { } } else { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } @@ -119,7 +119,7 @@ impl TokenCacheStore for ClientCertificateCredential { if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { @@ -127,7 +127,7 @@ impl TokenCacheStore for ClientCertificateCredential { } } else { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 9feeb457..00f450a7 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -7,13 +7,13 @@ use url::Url; use uuid::Uuid; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; -use graph_extensions::cache::{InMemoryCredentialStore, TokenCacheStore}; +use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, - ForceTokenRefresh, MsalToken, TokenCredentialExecutor, + ForceTokenRefresh, Token, TokenCredentialExecutor, }; credential_builder!( @@ -50,7 +50,7 @@ pub struct ClientSecretCredential { /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, - token_cache: InMemoryCredentialStore<MsalToken>, + token_cache: InMemoryTokenStore<Token>, } impl Debug for ClientSecretCredential { @@ -69,7 +69,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), - token_cache: InMemoryCredentialStore::new(), + token_cache: InMemoryTokenStore::new(), } } @@ -83,7 +83,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), - token_cache: InMemoryCredentialStore::new(), + token_cache: InMemoryTokenStore::new(), } } @@ -96,14 +96,14 @@ impl ClientSecretCredential { #[async_trait] impl TokenCacheStore for ClientSecretCredential { - type Token = MsalToken; + type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } else { @@ -111,7 +111,7 @@ impl TokenCacheStore for ClientSecretCredential { } } else { let response = self.execute()?; - let msal_token: MsalToken = response.json()?; + let msal_token: Token = response.json()?; self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) } @@ -123,7 +123,7 @@ impl TokenCacheStore for ClientSecretCredential { if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; tracing::debug!("tokenResponse={:#?}", &msal_token); self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) @@ -133,7 +133,7 @@ impl TokenCacheStore for ClientSecretCredential { } } else { let response = self.execute_async().await?; - let msal_token: MsalToken = response.json().await?; + let msal_token: Token = response.json().await?; tracing::debug!("tokenResponse={:#?}", &msal_token); self.token_cache.store(cache_id, msal_token.clone()); Ok(msal_token) @@ -232,7 +232,7 @@ impl ClientSecretCredentialBuilder { client_secret: client_secret.as_ref().to_string(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: Default::default(), - token_cache: InMemoryCredentialStore::new(), + token_cache: InMemoryTokenStore::new(), }, } } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 22db49ea..dfc4a864 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -3,10 +3,8 @@ use std::fmt::Debug; use async_trait::async_trait; use dyn_clone::DynClone; -use http::header::ACCEPT; -use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::header::HeaderMap; use reqwest::tls::Version; -use reqwest::ClientBuilder; use tracing::debug; use url::Url; use uuid::Uuid; @@ -14,7 +12,7 @@ use uuid::Uuid; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::AuthorizationRequest; +use crate::identity::AuthorizationRequestParts; use crate::identity::{Authority, AzureCloudInstance}; dyn_clone::clone_trait_object!(TokenCredentialExecutor); @@ -25,20 +23,92 @@ pub trait TokenCredentialExecutor: DynClone + Debug { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; - fn authorization_request_parts(&mut self) -> IdentityResult<AuthorizationRequest> { + fn authorization_request_parts(&mut self) -> IdentityResult<AuthorizationRequestParts> { let uri = self.uri()?; let form = self.form_urlencode()?; let basic_auth = self.basic_auth(); let extra_headers = self.extra_header_parameters(); let extra_query_params = self.extra_query_parameters(); - let mut auth_request = AuthorizationRequest::new(uri, form, basic_auth); + let mut auth_request = AuthorizationRequestParts::new(uri, form, basic_auth); auth_request.with_extra_headers(extra_headers); auth_request.with_extra_query_parameters(extra_query_params); Ok(auth_request) } + #[tracing::instrument] + fn build(&mut self) -> AuthExecutionResult<reqwest::blocking::RequestBuilder> { + let http_client = reqwest::blocking::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + let auth_request = self.authorization_request_parts()?; + let basic_auth = auth_request.basic_auth; + + if let Some((client_identifier, secret)) = basic_auth { + let request_builder = http_client + .post(auth_request.uri) + .basic_auth(client_identifier, Some(secret)) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + Ok(request_builder) + } else { + let request_builder = http_client + .post(auth_request.uri) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + Ok(request_builder) + } + } + + #[tracing::instrument] + fn build_async(&mut self) -> AuthExecutionResult<reqwest::RequestBuilder> { + let http_client = reqwest::ClientBuilder::new() + .min_tls_version(Version::TLS_1_2) + .https_only(true) + .build()?; + + let auth_request = self.authorization_request_parts()?; + let basic_auth = auth_request.basic_auth; + + if let Some((client_identifier, secret)) = basic_auth { + let request_builder = http_client + .post(auth_request.uri) + .basic_auth(client_identifier, Some(secret)) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + Ok(request_builder) + } else { + let request_builder = http_client + .post(auth_request.uri) + .headers(auth_request.headers) + .form(&auth_request.form_urlencoded); + + debug!( + "authorization request constructed; request={:#?}", + request_builder + ); + Ok(request_builder) + } + } + fn client_id(&self) -> &Uuid { &self.app_config().client_id } @@ -65,6 +135,24 @@ pub trait TokenCredentialExecutor: DynClone + Debug { &self.app_config().extra_query_parameters } + #[tracing::instrument] + fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { + let request_builder = self.build()?; + let response = request_builder.send()?; + debug!("authorization response received; response={:#?}", response); + Ok(response) + } + + #[tracing::instrument] + async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { + let request_builder = self.build_async()?; + let response = request_builder.send().await?; + debug!("authorization response received; response={:#?}", response); + Ok(response) + } +} + +/* fn openid_configuration_url(&self) -> IdentityResult<Url> { Ok(Url::parse( format!( @@ -114,72 +202,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { Ok(response) } - fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - - let auth_request = self.authorization_request_parts()?; - let basic_auth = auth_request.basic_auth; - if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client - .post(auth_request.uri) - .basic_auth(client_identifier, Some(secret)) - .headers(auth_request.headers) - .form(&auth_request.form_urlencoded) - .send()?) - } else { - Ok(http_client - .post(auth_request.uri) - .form(&auth_request.form_urlencoded) - .send()?) - } - } - - #[tracing::instrument] - async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { - //let mut uri = self.uri()?; - // let form = self.form_urlencode()?; - let http_client = ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - - let auth_request = self.authorization_request_parts()?; - let basic_auth = auth_request.basic_auth; - if let Some((client_identifier, secret)) = basic_auth { - let request_builder = http_client - .post(auth_request.uri) - .basic_auth(client_identifier, Some(secret)) - .headers(auth_request.headers) - .form(&auth_request.form_urlencoded); - - debug!( - "authorization request constructed; request={:#?}", - request_builder - ); - let response = request_builder.send().await; - debug!("authorization response received; response={:#?}", response); - Ok(response?) - } else { - let request_builder = http_client - .post(auth_request.uri) - .headers(auth_request.headers) - .form(&auth_request.form_urlencoded); - - debug!( - "authorization request constructed; request={:#?}", - request_builder - ); - let response = request_builder.send().await; - debug!("authorization response received; response={:#?}", response); - Ok(response?) - } - } -} - -#[cfg(test)] + #[cfg(test)] mod test { use super::*; use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; @@ -211,3 +234,5 @@ mod test { ) } } + + */ diff --git a/graph-oauth/src/identity/msal_token.rs b/graph-oauth/src/identity/msal_token.rs index b1b4dba5..3a4f464b 100644 --- a/graph-oauth/src/identity/msal_token.rs +++ b/graph-oauth/src/identity/msal_token.rs @@ -58,7 +58,7 @@ struct PhantomMsalToken { /// # use graph_extensions::token::MsalToken; /// let token_response = MsalToken::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); /// ``` -/// The [MsalToken::jwt] method attempts to parse the access token as a JWT. +/// The [Token::jwt] method attempts to parse the access token as a JWT. /// Tokens returned for personal microsoft accounts that use legacy MSA /// are encrypted and cannot be parsed. This bearer token may still be /// valid but the jwt() method will return None. @@ -66,7 +66,7 @@ struct PhantomMsalToken { /// [Microsoft identity platform access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// ``` #[derive(Clone, Eq, PartialEq, Serialize)] -pub struct MsalToken { +pub struct Token { pub access_token: String, pub token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] @@ -98,17 +98,17 @@ pub struct MsalToken { log_pii: bool, } -impl MsalToken { +impl Token { pub fn new<T: ToString, I: IntoIterator<Item = T>>( token_type: &str, expires_in: i64, access_token: &str, scope: I, - ) -> MsalToken { + ) -> Token { let timestamp = time::OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(expires_in)); - MsalToken { + Token { token_type: token_type.into(), ext_expires_in: None, expires_in, @@ -232,9 +232,9 @@ impl MsalToken { /// /// # Example /// ``` - /// # use graph_extensions::token::{MsalToken, IdToken}; + /// # use graph_oauth::identity::{Token, IdToken}; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); /// ``` pub fn with_id_token(&mut self, id_token: IdToken) { @@ -262,7 +262,7 @@ impl MsalToken { /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled - /// passing [MsalToken] to logging or print functions will log both the bearer + /// passing [Token] to logging or print functions will log both the bearer /// access token value, the refresh token value if any, and the id token value. /// By default these do not get logged. pub fn enable_pii_logging(&mut self, log_pii: bool) { @@ -283,7 +283,7 @@ impl MsalToken { /// from when the token was first retrieved. /// /// This will reset the the timestamp from Utc Now + expires_in. This means - /// that if calling [MsalToken::gen_timestamp] will only be reliable if done + /// that if calling [Token::gen_timestamp] will only be reliable if done /// when the access token is first retrieved. /// /// @@ -357,9 +357,9 @@ impl MsalToken { } } -impl Default for MsalToken { +impl Default for Token { fn default() -> Self { - MsalToken { + Token { token_type: String::new(), expires_in: 0, ext_expires_in: None, @@ -381,19 +381,19 @@ impl Default for MsalToken { } } -impl ToString for MsalToken { +impl ToString for Token { fn to_string(&self) -> String { self.access_token.to_string() } } -impl AsBearer for MsalToken { +impl AsBearer for Token { fn as_bearer(&self) -> String { self.access_token.to_string() } } -impl TryFrom<&str> for MsalToken { +impl TryFrom<&str> for Token { type Error = GraphFailure; fn try_from(value: &str) -> Result<Self, Self::Error> { @@ -401,35 +401,35 @@ impl TryFrom<&str> for MsalToken { } } -impl TryFrom<reqwest::blocking::RequestBuilder> for MsalToken { +impl TryFrom<reqwest::blocking::RequestBuilder> for Token { type Error = GraphFailure; fn try_from(value: reqwest::blocking::RequestBuilder) -> Result<Self, Self::Error> { let response = value.send()?; - MsalToken::try_from(response) + Token::try_from(response) } } -impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for MsalToken { +impl TryFrom<Result<reqwest::blocking::Response, reqwest::Error>> for Token { type Error = GraphFailure; fn try_from( value: Result<reqwest::blocking::Response, reqwest::Error>, ) -> Result<Self, Self::Error> { let response = value?; - MsalToken::try_from(response) + Token::try_from(response) } } -impl TryFrom<reqwest::blocking::Response> for MsalToken { +impl TryFrom<reqwest::blocking::Response> for Token { type Error = GraphFailure; fn try_from(value: reqwest::blocking::Response) -> Result<Self, Self::Error> { - Ok(value.json::<MsalToken>()?) + Ok(value.json::<Token>()?) } } -impl fmt::Debug for MsalToken { +impl fmt::Debug for Token { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.log_pii { f.debug_struct("MsalAccessToken") @@ -472,13 +472,13 @@ impl fmt::Debug for MsalToken { } } -impl AsRef<str> for MsalToken { +impl AsRef<str> for Token { fn as_ref(&self) -> &str { self.access_token.as_str() } } -impl<'de> Deserialize<'de> for MsalToken { +impl<'de> Deserialize<'de> for Token { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, @@ -488,7 +488,7 @@ impl<'de> Deserialize<'de> for MsalToken { let timestamp = OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); - Ok(MsalToken { + Ok(Token { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, expires_in: phantom_access_token.expires_in, @@ -514,12 +514,12 @@ mod test { #[test] fn is_expired_test() { - let mut access_token = MsalToken::default(); + let mut access_token = Token::default(); access_token.with_expires_in(5); std::thread::sleep(std::time::Duration::from_secs(6)); assert!(access_token.is_expired()); - let mut access_token = MsalToken::default(); + let mut access_token = Token::default(); access_token.with_expires_in(8); std::thread::sleep(std::time::Duration::from_secs(4)); assert!(!access_token.is_expired()); @@ -551,7 +551,7 @@ mod test { #[test] pub fn test_deserialize() { - let _token: MsalToken = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); - let _token: MsalToken = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); + let _token: Token = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); + let _token: Token = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); } } diff --git a/src/client/graph.rs b/src/client/graph.rs index 90e887d1..6fecc4a0 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -44,7 +44,7 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AllowedHostValidator, HostValidator, MsalToken}; +use crate::oauth::{AllowedHostValidator, HostValidator, Token}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -534,8 +534,8 @@ impl From<String> for Graph { } } -impl From<&MsalToken> for Graph { - fn from(token: &MsalToken) -> Self { +impl From<&Token> for Graph { + fn from(token: &Token) -> Self { Graph::from_client_app(BearerTokenCredential::new(token.access_token.clone())) } } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index e480a4d8..270036be 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -3,8 +3,8 @@ use from_as::*; use graph_core::resource::ResourceIdentity; use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, MsalToken, - ResourceOwnerPasswordCredential, TokenCredentialExecutor, + ClientSecretCredential, ConfidentialClientApplication, ResourceOwnerPasswordCredential, Token, + TokenCredentialExecutor, }; use graph_rs_sdk::Graph; use std::collections::{BTreeMap, HashMap}; @@ -12,7 +12,7 @@ use std::convert::TryFrom; use std::env; use std::io::{Read, Write}; -use graph_http::api_impl::BearerToken; +use graph_core::identity::ClientApplication; use std::sync::Mutex; // static mutex's that are used for preventing test failures @@ -150,13 +150,13 @@ pub enum OAuthTestClient { } impl OAuthTestClient { - fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, MsalToken)> { + fn get_access_token(&self, creds: OAuthTestCredentials) -> Option<(String, Token)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); if let Ok(response) = credential.execute() { - let token: MsalToken = response.json().unwrap(); + let token: Token = response.json().unwrap(); Some((user_id, token)) } else { None @@ -165,7 +165,7 @@ impl OAuthTestClient { OAuthTestClient::ResourceOwnerPasswordCredentials => { let mut credential = creds.resource_owner_password_credential(); if let Ok(response) = credential.execute() { - let token: MsalToken = response.json().unwrap(); + let token: Token = response.json().unwrap(); Some((user_id, token)) } else { None @@ -182,17 +182,14 @@ impl OAuthTestClient { creds.client_credentials() } - async fn get_access_token_async( - &self, - creds: OAuthTestCredentials, - ) -> Option<(String, MsalToken)> { + async fn get_access_token_async(&self, creds: OAuthTestCredentials) -> Option<(String, Token)> { let user_id = creds.user_id.clone()?; match self { OAuthTestClient::ClientCredentials => { let mut credential = creds.client_credentials(); match credential.execute_async().await { Ok(response) => { - let token: MsalToken = response.json().await.unwrap(); + let token: Token = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -202,7 +199,7 @@ impl OAuthTestClient { let mut credential = creds.resource_owner_password_credential(); match credential.execute_async().await { Ok(response) => { - let token: MsalToken = response.json().await.unwrap(); + let token: Token = response.json().await.unwrap(); Some((user_id, token)) } Err(_) => None, @@ -212,7 +209,21 @@ impl OAuthTestClient { } } - pub fn request_access_token(&self) -> Option<(String, MsalToken)> { + fn get_credential( + &self, + creds: OAuthTestCredentials, + ) -> Option<(String, impl ClientApplication)> { + let user_id = creds.user_id.clone()?; + match self { + OAuthTestClient::ClientCredentials => { + let credential = creds.client_credentials(); + Some((user_id, credential)) + } + _ => None, + } + } + + pub fn request_access_token(&self) -> Option<(String, Token)> { if Environment::is_local() || Environment::is_travis() { let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); let test_client_map = OAuthTestClientMap { @@ -230,7 +241,23 @@ impl OAuthTestClient { } } - pub async fn request_access_token_async(&self) -> Option<(String, MsalToken)> { + pub fn request_access_token_credential(&self) -> Option<(String, impl ClientApplication)> { + if Environment::is_local() || Environment::is_travis() { + let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_credential(test_client_map.get(self).unwrap()) + } else if Environment::is_github() { + let map: OAuthTestClientMap = + serde_json::from_str(&env::var("TEST_CREDENTIALS").unwrap()).unwrap(); + self.get_credential(map.get(self).unwrap()) + } else { + None + } + } + + pub async fn request_access_token_async(&self) -> Option<(String, Token)> { if Environment::is_local() || Environment::is_travis() { let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); let test_client_map = OAuthTestClientMap { @@ -301,30 +328,30 @@ impl OAuthTestClient { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, credentials) = client.default_client()?; - if let Some((id, token)) = test_client.get_access_token_async(credentials).await { - Some((id, Graph::from_client_app(BearerToken(token.access_token)))) + if let Some((id, client_application)) = test_client.get_credential(credentials) { + Some((id, Graph::from_client_app(client_application))) } else { None } } pub fn graph(&self) -> Option<(String, Graph)> { - if let Some((id, token)) = self.request_access_token() { - Some((id, Graph::from_client_app(BearerToken(token.access_token)))) + if let Some((id, client_application)) = self.request_access_token_credential() { + Some((id, Graph::from_client_app(client_application))) } else { None } } pub async fn graph_async(&self) -> Option<(String, Graph)> { - if let Some((id, token)) = self.request_access_token_async().await { - Some((id, Graph::from_client_app(BearerToken(token.access_token)))) + if let Some((id, client_application)) = self.request_access_token_credential() { + Some((id, Graph::from_client_app(client_application))) } else { None } } - pub fn token(resource_identity: ResourceIdentity) -> Option<MsalToken> { + pub fn token(resource_identity: ResourceIdentity) -> Option<Token> { let app_registration = OAuthTestClient::get_app_registration()?; let client = app_registration.get_by_resource_identity(resource_identity)?; let (test_client, _credentials) = client.default_client()?; From 69bf42d3f075ae489663c5750f268e0507aaace4 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 28 Oct 2023 00:00:58 -0400 Subject: [PATCH 050/118] Add examples and readme updates --- README.md | 217 +++++++++++++++++- examples/interactive_authentication/main.rs | 5 + .../interactive_authentication/web_view.rs | 75 ++++++ examples/oauth/README.md | 22 +- .../auth_code_grant/auth_code_grant_secret.rs | 60 +++-- .../interactive_authentication.rs | 68 ++++++ examples/oauth/auth_code_grant/mod.rs | 2 + examples/oauth/client_credentials/mod.rs | 28 +-- examples/oauth/device_code.rs | 5 +- .../openid_connect_form_post.rs | 2 +- .../auth_code_grant.rs | 2 +- .../oauth_authorization_url/openid_connect.rs | 2 +- 12 files changed, 444 insertions(+), 44 deletions(-) create mode 100644 examples/interactive_authentication/main.rs create mode 100644 examples/interactive_authentication/web_view.rs create mode 100644 examples/oauth/auth_code_grant/interactive_authentication.rs diff --git a/README.md b/README.md index 0a482250..c489e087 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ ### Available on [crates.io](https://crates.io/crates/graph-rs-sdk) +The crate also provides an oauth and openid connect client for get getting access tokens +and now supports interactive authentication use web view. + ```toml graph-rs-sdk = "1.1.1" tokio = { version = "1.25.0", features = ["full"] } @@ -49,7 +52,10 @@ Other than that feel free to ask questions, provide tips to others, and talk abo ## Table Of Contents * [Usage](#usage) - * [Authentication and Authorization](#authentication-and-authorization-in-active-development) + * [OAuth - Getting Access Tokens](#oauth---getting-access-tokens) + * [Identity Platform Support](#identity-platform-support) + * [Automatic Token Refresh](#automatic-token-refresh) + * [Interactive Authentication](#interactive-authentication) * [Async and Blocking Client](#async-and-blocking-client) * [Async Client](#async-client-default) * [Blocking Client](#blocking-client) @@ -989,3 +995,212 @@ async fn get_user() -> GraphResult<()> { Ok(()) } ``` + +## OAuth - Getting Access Tokens + +Use application builders to store your auth configuration and have the client +handle the access token requests for you. + +Support for: + +- Automatic Token Refresh +- Interactive Authentication +- Device Code Polling +- Authorization Using Certificates + +There are two main types for building your chosen OAuth or OpenId Connect Flow. + +- `PublicClientApplication` +- `ConfidentialClientApplication` + +Once you have built a `ConfidentialClientApplication` or a `PublicClientApplication` +you can pass these to the graph client. + +Automatic token refresh is also done by passing the `ConfidentialClientApplication` or the +`PublicClientApplication` to the `Graph` client. + +For more extensive examples see the +[OAuth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth) in the examples/oauth +directory on [GitHub](https://github.com/sreeise/graph-rs-sdk). + + +```rust,ignore +let confidental_client: ConfidentialClientApplication<ClientSecretCredential> = ... + +let graph_client = Graph::from(confidential_client); +``` + +### Identity Platform Support + +The following flows from the Microsoft Identity Platform are supported: + +- [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Authorization Code Grant Certificate](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) +- [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) +- [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) +- [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +- [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) + +You can use the url builders for those flows that require an authorization code using a redirect after sign in you can use + +### Examples + +### Authorization Code Grant + +The authorization code grant is considered a confidential client (except in the hybrid flow) +and we can get an access token by using the authorization code returned in the query of the URL +on redirect after sign in is performed by the user. + +Once you have the authorization code you can pass this to the client and the client +will perform the request to get an access token on the first graph api call that you make. + +```rust +use graph_rs_sdk::{ + Graph, + oauth::ConfidentialClientApplication, +}; + +#[tokio::main] +async fn main() { + let authorization_code = "<AUTH_CODE>"; + let client_id = "<CLIENT_ID>"; + let client_secret = "<CLIENT_SECRET>"; + let scope = vec!["<SCOPE>", "<SCOPE>"]; + let redirect_uri = "http://localhost:8080"; + + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .build(); + + let graph_client = Graph::from(confidential_client); + + let _response = graph_client + .users() + .list_user() + .send() // Also makes first access token request at this point + .await; +} +``` + +### Client Credentials Grant. + +The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, +instead of impersonating a user, to authenticate when calling another web service. The grant specified in RFC 6749, +sometimes called two-legged OAuth, can be used to access web-hosted resources by using the identity of an application. +This type is commonly used for server-to-server interactions that must run in the background, without immediate +interaction with a user, and is often referred to as daemons or service accounts. + +Client credentials flow requires a one time administrator acceptance +of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions +as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) + +```rust +use graph_rs_sdk::{ + oauth::ConfidentialClientApplication, Graph +}; + +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static TENANT_ID: &str = "<TENANT_ID>"; + +pub async fn get_graph_client() -> Graph { + let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) + .build(); + + Graph::from(confidential_client_application) +} +``` + + +### Automatic Token Refresh + +Using automatic token refresh requires getting a refresh token as part of the token response. +To get a refresh token you must include the `offline_access` scope. + +Automatic token refresh is done by passing the `ConfidentialClientApplication` or the +`PublicClientApplication` to the `Graph` client. + +If you are using the `client credentials` grant you do not need the `offline_access` scope. +Tokens will still be automatically refreshed as this flow does not require using a refresh token to get +a new access token. + +```rust +async fn authenticate() { + let scope = vec!["offline_access"]; + let mut credential_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .auth_code_url_builder() + .interactive_authentication(None) // Open web view for interactive authentication sign in + .unwrap(); + // ... add any other parameters you need + + let confidential_client = credential_builder.with_client_secret(CLIENT_SECRET) + .build(); + + let client = Graph::from(&confidential_client); +} +``` + + +### Interactive Authentication + +Requires Feature `interactive_auth` + +```toml +[dependencies] +graph-rs-sdk = { version = "...", features = ["interactive_auth"] } +``` + +Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) crate to run web view on +platforms that support it such as on a desktop. + +```rust +use graph_rs_sdk::oauth::{ + web::Theme, web::WebViewOptions, AuthorizationCodeCredential, + ConfidentialClientApplication +}; +use graph_rs_sdk::Graph; + +fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { + let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .auth_code_url_builder() + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .interactive_authentication(None) + .unwrap(); + + confidential_client_builder.with_client_secret(CLIENT_SECRET).build() +} + +async fn authenticate() { + // Create a tracing subscriber to log debug/trace events coming from + // authorization http calls and the Graph client. + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::TRACE) + .init(); + + let mut confidential_client = run_interactive_auth(); + + let client = Graph::from(&confidential_client); + + let response = client.user(USER_ID) + .get_user() + .send() + .await + .unwrap(); + + println!("{response:#?}"); + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); +} +``` diff --git a/examples/interactive_authentication/main.rs b/examples/interactive_authentication/main.rs new file mode 100644 index 00000000..22c02c7d --- /dev/null +++ b/examples/interactive_authentication/main.rs @@ -0,0 +1,5 @@ +#![allow(dead_code, unused, unused_imports)] + +mod web_view; + +fn main() {} diff --git a/examples/interactive_authentication/web_view.rs b/examples/interactive_authentication/web_view.rs new file mode 100644 index 00000000..d1fef07c --- /dev/null +++ b/examples/interactive_authentication/web_view.rs @@ -0,0 +1,75 @@ +use graph_rs_sdk::oauth::{ + web::Theme, web::WebViewOptions, AuthorizationCodeCredential, TokenCredentialExecutor, +}; +use graph_rs_sdk::Graph; + +static CLIENT_ID: &str = "CLIENT_ID"; +static CLIENT_SECRET: &str = "CLIENT_SECRET"; +static TENANT_ID: &str = "TENANT_ID"; + +// This should be the user id for the user you are logging in as. +static USER_ID: &str = "USER_ID"; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +// Requires feature=interactive_authentication + +// Interactive Authentication WebView Using Wry library https://github.com/tauri-apps/wry +// See the wry documentation for platform specific installation. Linux and macOS require +// installation of platform specific dependencies. These are not included by default. + +// This example executes the Authorization Code OAuth flow and handles +// sign in/redirect using WebView as well as authorization and token retrieval. + +// The WebView window will load on the sign in page for Microsoft Graph +// Log in with a user and upon redirect the window will close automatically. +// The credential_builder will store the authorization code returned on the +// redirect url after logging in and then build a ConfidentialClient<AuthorizationCodeCredential> + +// The ConfidentialClient<AuthorizationCodeCredential> handles authorization to get an access token +// on the first request made using the Graph client. The token is stored in an in memory cache +// and subsequent calls will use this token. If a refresh token is included, which you can get +// by requesting the offline_access scope, then the confidential client will take care of refreshing +// the token. +async fn authenticate() { + // Create a tracing subscriber to log debug/trace events coming from + // authorization http calls and the Graph client. + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::TRACE) + .init(); + + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .interactive_authentication(None) + .unwrap(); + + let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); + + let client = Graph::from(&confidential_client); + + let response = client.user(USER_ID).get_user().send().await.unwrap(); + + println!("{response:#?}"); + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); +} + +async fn customize_webview() { + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .interactive_authentication(Some( + WebViewOptions::builder() + .with_window_title("Sign In") + .with_theme(Theme::Dark) + .with_close_window_on_invalid_navigation(true), + )) + .unwrap(); +} diff --git a/examples/oauth/README.md b/examples/oauth/README.md index e3e7dc52..4e5bca65 100644 --- a/examples/oauth/README.md +++ b/examples/oauth/README.md @@ -10,14 +10,16 @@ There are two main types for building your chosen OAuth or OpenId Connect Flow. The authorization code grant is considered a confidential client (except in the hybrid flow) and we can get an access token by using the authorization code returned in the query of the URL -on redirect after authorization sign in is performed by the user. +on redirect after sign in is performed by the user. ```rust -use graph_rs_sdk::oauth::{ - ConfidentialClientApplication, +use graph_rs_sdk::{ + Graph, + oauth::ConfidentialClientApplication, }; -fn main() { +#[tokio::main] +async fn main() { let authorization_code = "<AUTH_CODE>"; let client_id = "<CLIENT_ID>"; let client_secret = "<CLIENT_SECRET>"; @@ -27,9 +29,17 @@ fn main() { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) - .with_scope(SCOPE.clone()) - .with_redirect_uri(REDIRECT_URI) + .with_scope(scope) + .with_redirect_uri(redirect_uri) .unwrap() .build(); + + let graph_client = Graph::from(confidential_client); + + let _response = graph_client + .users() + .list_user() + .send() + .await; } ``` diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index e1cd97c6..edb8fe89 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -81,31 +81,27 @@ async fn handle_redirect( // Callers should handle the Result from requesting an access token // in case of an error here. let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_authorization_code(authorization_code) + .with_auth_code(authorization_code) .with_client_secret(CLIENT_SECRET) .with_scope(vec![SCOPE]) .with_redirect_uri(REDIRECT_URI) .unwrap() .build(); - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); + let client = Graph::from(confidential_client); + let result = client.users().list_user().send().await; - if response.status().is_success() { - let mut access_token: Token = response.json().await.unwrap(); + match result { + Ok(response) => { + println!("{response:#?}"); - // Enables the printing of the bearer, refresh, and id token. - access_token.enable_pii_logging(true); - println!("{:#?}", access_token); - - // This will print the actual access token to the console. - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<ErrorMessage> = response.json().await; - - match result { - Ok(error_message) => println!("{error_message:#?}"), - Err(err) => println!("Error on deserialization:\n{err:#?}"), + let status = response.status(); + let body: serde_json::Value = response.json().await.unwrap(); + println!("Status: {status:#?}"); + println!("Body: {body:#?}"); + } + Err(err) => { + println!("{err:#?}"); } } @@ -117,3 +113,33 @@ async fn handle_redirect( None => Err(warp::reject()), } } +/* + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_auth_code(authorization_code) + .with_client_secret(CLIENT_SECRET) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) + .unwrap() + .build(); + + let response = confidential_client.execute_async().await.unwrap(); + println!("{response:#?}"); + + if response.status().is_success() { + let mut access_token: Token = response.json().await.unwrap(); + + // Enables the printing of the bearer, refresh, and id token. + access_token.enable_pii_logging(true); + println!("{:#?}", access_token); + + // This will print the actual access token to the console. + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<ErrorMessage> = response.json().await; + + match result { + Ok(error_message) => println!("{error_message:#?}"), + Err(err) => println!("Error on deserialization:\n{err:#?}"), + } + } +*/ diff --git a/examples/oauth/auth_code_grant/interactive_authentication.rs b/examples/oauth/auth_code_grant/interactive_authentication.rs new file mode 100644 index 00000000..fb0ad837 --- /dev/null +++ b/examples/oauth/auth_code_grant/interactive_authentication.rs @@ -0,0 +1,68 @@ +use graph_oauth::oauth::ConfidentialClientApplication; +use graph_rs_sdk::oauth::{ + web::Theme, web::WebViewOptions, AuthorizationCodeCredential, + AuthorizationCodeCredentialBuilder, TokenCredentialExecutor, +}; +use graph_rs_sdk::Graph; + +static CLIENT_ID: &str = "CLIENT_ID"; +static CLIENT_SECRET: &str = "CLIENT_SECRET"; +static TENANT_ID: &str = "TENANT_ID"; + +// This should be the user id for the user you are logging in as. +static USER_ID: &str = "USER_ID"; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +// Requires feature=interactive_authentication + +// Interactive Authentication WebView Using Wry library https://github.com/tauri-apps/wry +// See the wry documentation for platform specific installation. Linux and macOS require +// installation of platform specific dependencies. These are not included by default. + +// This example executes the Authorization Code OAuth flow and handles +// sign in/redirect using WebView as well as authorization and token retrieval. + +// The WebView window will load on the sign in page for Microsoft Graph +// Log in with a user and upon redirect the window will close automatically. +// The credential_builder will store the authorization code returned on the +// redirect url after logging in and then build a ConfidentialClient<AuthorizationCodeCredential> + +// The ConfidentialClient<AuthorizationCodeCredential> handles authorization to get an access token +// on the first request made using the Graph client. The token is stored in an in memory cache +// and subsequent calls will use this token. If a refresh token is included, which you can get +// by requesting the offline_access scope, then the confidential client will take care of refreshing +// the token. + +fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { + let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .auth_code_url_builder() + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .interactive_authentication(None) + .unwrap(); + + confidential_client_builder.with_client_secret(CLIENT_SECRET).build() +} + +async fn authenticate() { + // Create a tracing subscriber to log debug/trace events coming from + // authorization http calls and the Graph client. + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::TRACE) + .init(); + + let confidential_client = run_interactive_auth(); + + let client = Graph::from(&confidential_client); + + let response = client.user(USER_ID).get_user().send().await.unwrap(); + + println!("{response:#?}"); + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); +} diff --git a/examples/oauth/auth_code_grant/mod.rs b/examples/oauth/auth_code_grant/mod.rs index 1a40cb11..b3d76aaf 100644 --- a/examples/oauth/auth_code_grant/mod.rs +++ b/examples/oauth/auth_code_grant/mod.rs @@ -1,3 +1,5 @@ pub mod auth_code_grant_pkce; pub mod auth_code_grant_refresh_token; pub mod auth_code_grant_secret; + +pub mod interactive_authentication; diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index 96a5f203..080801ea 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -9,14 +9,14 @@ // to approve your application to call Microsoft Graph Apis on behalf of a user. Admin consent // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. -use graph_rs_sdk::oauth::{ - ClientSecretCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, - TokenRequest, -}; mod client_credentials_admin_consent; pub use client_credentials_admin_consent::*; +use graph_rs_sdk::{ + oauth::ClientSecretCredential, oauth::ConfidentialClientApplication, + oauth::TokenCredentialExecutor, Graph, +}; // This example shows programmatically getting an access token using the client credentials // flow after admin consent has been granted. If you have not granted admin consent, see @@ -27,26 +27,20 @@ static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; static TENANT_ID: &str = "<TENANT_ID>"; -pub async fn get_token_silent() { - let client_secret_credential = ConfidentialClientApplication::builder(CLIENT_ID) +pub async fn get_graph_client() -> Graph { + let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) .with_client_secret(CLIENT_SECRET) .with_tenant(TENANT_ID) .build(); - let mut confidential_client_application = - ConfidentialClientApplication::from(client_secret_credential); - - let response = confidential_client_application - .execute_async() - .await - .unwrap(); - println!("{response:#?}"); - let body: Token = response.json().await.unwrap(); + Graph::from(confidential_client_application) } -pub async fn get_token_silent2() { +/* +pub async fn get_token_silent() { let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) .build(); let response = confidential_client.execute_async().await.unwrap(); @@ -54,3 +48,5 @@ pub async fn get_token_silent2() { let body: Token = response.json().await.unwrap(); } + + */ diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 01087ba7..70f157ef 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -1,8 +1,10 @@ +use graph_oauth::oauth::ClientSecretCredential; use graph_rs_sdk::oauth::{ DeviceCodeCredential, DeviceCodeCredentialBuilder, PublicClientApplication, Token, TokenCredentialExecutor, }; use graph_rs_sdk::GraphResult; +use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; use std::time::Duration; use warp::hyper::body::HttpBody; @@ -15,7 +17,8 @@ static TENANT: &str = "<TENANT>"; // Poll the device code endpoint to get the code and a url that the user must // go to in order to enter the code. Polling will continue until either the user -// has entered the and an access token is returned or an error happens. +// has entered the code. Once a successful code has been entered the next time the +// device code endpoint is polled an access token is returned. fn poll_device_code() { let mut device_executor = PublicClientApplication::builder(CLIENT_ID) .with_device_code_authorization_executor() diff --git a/examples/oauth/openid_connect/openid_connect_form_post.rs b/examples/oauth/openid_connect/openid_connect_form_post.rs index 232382d5..fe3ab525 100644 --- a/examples/oauth/openid_connect/openid_connect_form_post.rs +++ b/examples/oauth/openid_connect/openid_connect_form_post.rs @@ -2,7 +2,7 @@ use graph_oauth::identity::{ ConfidentialClientApplication, Prompt, ResponseMode, ResponseType, TokenCredentialExecutor, TokenRequest, }; -use graph_oauth::oauth::{OpenIdAuthorizationUrl, OpenIdCredential}; +use graph_oauth::oauth::{OpenIdAuthorizationUrlParameters, OpenIdCredential}; use graph_rs_sdk::oauth::{IdToken, OAuthSerializer, Token}; use tracing_subscriber::fmt::format::FmtSpan; use url::Url; diff --git a/examples/oauth_authorization_url/auth_code_grant.rs b/examples/oauth_authorization_url/auth_code_grant.rs index 48fcc410..99ac5dcc 100644 --- a/examples/oauth_authorization_url/auth_code_grant.rs +++ b/examples/oauth_authorization_url/auth_code_grant.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::oauth::{ AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, Token, - TokenCredentialExecutor, TokenRequest, + TokenCredentialExecutor, }; static CLIENT_ID: &str = "<CLIENT_ID>"; diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index a4faaa90..37d672bf 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -20,7 +20,7 @@ fn open_id_authorization_url( scope: Vec<&str>, ) -> IdentityResult<Url> { ConfidentialClientApplication::builder(client_id) - .openid_authorization_url_builder() + .openid_url_builder() .with_tenant(tenant) .with_redirect_uri(redirect_uri)? .with_scope(scope) From 6515d4bf4b5be624d98b8aa153160d7de76645bd Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 30 Oct 2023 04:38:34 -0400 Subject: [PATCH 051/118] Use single trait for credential types in graph client and http client --- Cargo.toml | 18 +- README.md | 63 +-- .../auth_code_grant/auth_code_grant_pkce.rs | 111 ++--- .../auth_code_grant_refresh_token.rs | 37 -- .../auth_code_grant/auth_code_grant_secret.rs | 166 ++------ .../interactive_authentication.rs | 4 +- examples/oauth/auth_code_grant/mod.rs | 3 +- .../server_examples/auth_code_grant_pkce.rs | 113 +++++ .../server_examples/auth_code_grant_secret.rs | 114 ++++++ .../auth_code_grant/server_examples/mod.rs | 3 + .../client_credentials_admin_consent.rs | 19 +- examples/oauth/client_credentials/mod.rs | 23 +- examples/oauth/main.rs | 10 +- examples/oauth/openid/mod.rs | 3 + examples/oauth/openid/openid.rs | 43 ++ examples/oauth/openid/server_examples/mod.rs | 1 + .../server_examples/openid.rs} | 15 +- examples/oauth/openid_connect/mod.rs | 3 - .../client_credentials.rs | 3 +- .../legacy/implicit_grant.rs | 10 +- .../oauth_authorization_url/openid_connect.rs | 32 +- examples/paging/README.md | 44 ++ examples/{next_links => paging}/channel.rs | 0 examples/{next_links => paging}/delta.rs | 0 examples/{next_links => paging}/main.rs | 0 examples/{next_links => paging}/stream.rs | 0 .../{paging.rs => paging_and_next_links.rs} | 0 graph-core/Cargo.toml | 2 + .../src/cache/in_memory_cache_store.rs | 15 +- graph-core/src/cache/mod.rs | 5 + .../src/cache/token_cache.rs | 2 +- .../src/crypto/mod.rs | 0 .../src/crypto/pkce.rs | 0 graph-core/src/http/mod.rs | 2 + .../src/http/response_converter.rs | 0 graph-core/src/identity/client_application.rs | 13 +- graph-core/src/lib.rs | 2 + graph-extensions/Cargo.toml | 38 -- graph-extensions/src/cache/mod.rs | 5 - graph-extensions/src/http/mod.rs | 5 - .../src/http/response_builder_ext.rs | 53 --- graph-extensions/src/lib.rs | 3 - graph-http/Cargo.toml | 15 +- graph-http/src/client.rs | 55 ++- graph-http/src/lib.rs | 2 +- graph-oauth/Cargo.toml | 20 +- graph-oauth/README.md | 256 +++++++++--- graph-oauth/src/auth.rs | 387 ++++++++++-------- graph-oauth/src/grants.rs | 189 --------- .../identity/authorization_query_response.rs | 16 +- .../src/identity/credentials/app_config.rs | 37 +- .../credentials/application_builder.rs | 35 +- .../auth_code_authorization_url.rs | 108 +++-- ...authorization_code_assertion_credential.rs | 52 ++- ...thorization_code_certificate_credential.rs | 77 ++-- .../authorization_code_credential.rs | 38 +- .../credentials/bearer_token_credential.rs | 5 +- .../client_assertion_credential.rs | 12 +- .../credentials/client_builder_impl.rs | 21 +- .../client_certificate_credential.rs | 6 +- .../credentials/client_secret_credential.rs | 12 +- .../confidential_client_application.rs | 102 ++++- .../credentials/device_code_credential.rs | 6 +- .../credentials/legacy/implicit_credential.rs | 20 +- .../credentials/open_id_authorization_url.rs | 52 ++- .../credentials/open_id_credential.rs | 141 ++++++- .../credentials/public_client_application.rs | 8 +- .../credentials/token_credential_executor.rs | 110 +---- graph-oauth/src/identity/mod.rs | 4 +- .../src/identity/{msal_token.rs => token.rs} | 86 ++-- graph-oauth/src/lib.rs | 29 +- graph-oauth/src/oauth_error.rs | 19 +- .../src/web/interactive_authenticator.rs | 4 +- graph-oauth/src/web/interactive_web_view.rs | 33 +- graph-oauth/src/web/web_view_options.rs | 42 +- src/client/graph.rs | 64 ++- src/lib.rs | 4 +- test-tools/src/lib.rs | 1 - test-tools/src/oauth.rs | 75 ---- tests/oauth_tests.rs | 156 ------- tests/token_cache_tests.rs | 2 +- 81 files changed, 1752 insertions(+), 1532 deletions(-) delete mode 100644 examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs create mode 100644 examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs create mode 100644 examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs create mode 100644 examples/oauth/auth_code_grant/server_examples/mod.rs create mode 100644 examples/oauth/openid/mod.rs create mode 100644 examples/oauth/openid/openid.rs create mode 100644 examples/oauth/openid/server_examples/mod.rs rename examples/oauth/{openid_connect/openid_connect_form_post.rs => openid/server_examples/openid.rs} (92%) delete mode 100644 examples/oauth/openid_connect/mod.rs create mode 100644 examples/paging/README.md rename examples/{next_links => paging}/channel.rs (100%) rename examples/{next_links => paging}/delta.rs (100%) rename examples/{next_links => paging}/main.rs (100%) rename examples/{next_links => paging}/stream.rs (100%) rename examples/{paging.rs => paging_and_next_links.rs} (100%) rename graph-extensions/src/cache/in_memory_credential_store.rs => graph-core/src/cache/in_memory_cache_store.rs (60%) create mode 100644 graph-core/src/cache/mod.rs rename graph-extensions/src/cache/cache_store.rs => graph-core/src/cache/token_cache.rs (95%) rename {graph-extensions => graph-core}/src/crypto/mod.rs (100%) rename {graph-extensions => graph-core}/src/crypto/pkce.rs (100%) rename {graph-extensions => graph-core}/src/http/response_converter.rs (100%) delete mode 100644 graph-extensions/Cargo.toml delete mode 100644 graph-extensions/src/cache/mod.rs delete mode 100644 graph-extensions/src/http/mod.rs delete mode 100644 graph-extensions/src/http/response_builder_ext.rs delete mode 100644 graph-extensions/src/lib.rs delete mode 100644 graph-oauth/src/grants.rs rename graph-oauth/src/identity/{msal_token.rs => token.rs} (88%) delete mode 100644 test-tools/src/oauth.rs delete mode 100644 tests/oauth_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 2362cd20..1058ade2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ members = [ "graph-codegen", "graph-http", "graph-core", - "graph-extensions" ] [dependencies] @@ -36,13 +35,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" lazy_static = "1.4.0" +uuid = { version = "1.4.1", features = ["v4"] } graph-oauth = { path = "./graph-oauth", version = "1.0.2", default-features=false } graph-http = { path = "./graph-http", version = "1.1.0", default-features=false } graph-error = { path = "./graph-error", version = "0.2.2" } -graph-core = { path = "./graph-core", version = "0.4.0" } -graph-extensions = { path = "./graph-extensions", version = "0.1.0", default-features=false } -uuid = { version = "1.4.1", features = ["v4"] } +graph-core = { path = "./graph-core", version = "0.4.0", default-features=false } # When updating or adding new features to this or dependent crates run # cargo tree -e features -i graph-rs-sdk @@ -52,13 +50,13 @@ uuid = { version = "1.4.1", features = ["v4"] } [features] default = ["native-tls"] -native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls", "graph-extensions/native-tls"] -rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls", "graph-extensions/rustls-tls"] -brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli", "graph-extensions/brotli"] -deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate", "graph-extensions/deflate"] -trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns", "graph-extensions/trust-dns"] +native-tls = ["reqwest/native-tls", "graph-http/native-tls", "graph-oauth/native-tls", "graph-core/native-tls"] +rustls-tls = ["reqwest/rustls-tls", "graph-http/rustls-tls", "graph-oauth/rustls-tls", "graph-core/rustls-tls"] +brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli", "graph-core/brotli"] +deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate", "graph-core/deflate"] +trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns", "graph-core/trust-dns"] openssl = ["graph-oauth/openssl"] -interactive-auth = ["graph-oauth/interactive-auth"] +# interactive-auth = ["graph-oauth/interactive-auth"] [dev-dependencies] bytes = { version = "1.4.0" } diff --git a/README.md b/README.md index c489e087..e8f434ab 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,15 @@ ### Available on [crates.io](https://crates.io/crates/graph-rs-sdk) -The crate also provides an oauth and openid connect client for get getting access tokens -and now supports interactive authentication use web view. +Features: + +- Microsoft Graph V1 and Beta API Client + - Paging using Streaming, Channels, or Iterators. + - Upload Sessions, OData Queries, and File Downloads +- Microsoft Graph Identity Platform OAuth2 and OpenId Connect Client + - Auth Code, Client Credentials, Device Code, OpenId, X509 Certificates, PKCE + - Interactive Authentication + - Automatic Token Refresh ```toml graph-rs-sdk = "1.1.1" @@ -36,19 +43,6 @@ use futures::StreamExt; use graph_rs_sdk::*; ``` -Contributing and Wiki: -- [Contributions](https://github.com/sreeise/graph-rs-sdk/wiki/Contributing) -- [Wiki](https://github.com/sreeise/graph-rs-sdk/wiki) - -### Feature requests or Bug reports. - -For bug reports please file an issue on GitHub and a response or fix will be given as soon as possible. - -The [Discussions](https://github.com/sreeise/graph-rs-sdk/discussions) tab on [GitHub](https://github.com/sreeise/graph-rs-sdk/discussions) -is enabled so feel free to stop by there with any questions or feature requests as well. For bugs, please file -an issue first. Features can be requested through issues or discussions. Either way works. -Other than that feel free to ask questions, provide tips to others, and talk about the project in general. - ## Table Of Contents * [Usage](#usage) @@ -65,7 +59,9 @@ Other than that feel free to ask questions, provide tips to others, and talk abo * [Channels](#channels) * [API Usage](#api-usage) * [Id vs Non-Id methods](#id-vs-non-id-methods-such-as-useruser-id-vs-users) - * [Information about the project itself (contributor section coming soon)](#for-those-interested-in-the-code-itself-contributor-section-coming-soon) + * [Contributing](#contributing) + * [Wiki](#wiki) + * [Feature Requests for Bug Reports](#feature-requests-or-bug-reports) ### What APIs are available @@ -77,17 +73,6 @@ config but in general most of them are implemented. For extensive examples see the [examples directory on GitHub](https://github.com/sreeise/graph-rs-sdk/tree/master/examples) -### Authentication and Authorization (In Active Development) - -The crate is undergoing major development in order to support all or most scenarios in the Microsoft Identity Platform -where its possible to do so. Another goal is to make the authentication/authorization impl much easier to use -by providing easy to use clients that follow similar designs to other sdks for the Identity Platform. - -This includes token caches, automatic refresh of tokens, interactive web view authentication, and much more. -The development is well underway - as of right now no merge has gone into master but changes will be there soon -and they will almost certainly be unstable in some respects while we continue to work on this. However, the crate -on crates.io is currently only updated on stable version releases. - ### Async and Blocking Client The crate can do both an async and blocking requests. @@ -998,6 +983,10 @@ async fn get_user() -> GraphResult<()> { ## OAuth - Getting Access Tokens +The crate is undergoing major development in order to support all or most scenarios in the +Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some +unstable features. Any version that is not a pre-release version of the crate is considered stable. + Use application builders to store your auth configuration and have the client handle the access token requests for you. @@ -1204,3 +1193,23 @@ async fn authenticate() { println!("{body:#?}"); } ``` + + +## Contributing + +See the [Contributions](https://github.com/sreeise/graph-rs-sdk/wiki/Contributing) guide on GitHub + + +## Wiki: + +See the [GitHub Wiki](https://github.com/sreeise/graph-rs-sdk/wiki) + + +## Feature requests or Bug reports + +For bug reports please file an issue on GitHub and a response or fix will be given as soon as possible. + +The [Discussions](https://github.com/sreeise/graph-rs-sdk/discussions) tab on [GitHub](https://github.com/sreeise/graph-rs-sdk/discussions) +is enabled so feel free to stop by there with any questions or feature requests as well. For bugs, please file +an issue first. Features can be requested through issues or discussions. Either way works. +Other than that feel free to ask questions, provide tips to others, and talk about the project in general. diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index 8abfdc53..7da17845 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,10 +1,10 @@ -use graph_oauth::identity::ResponseType; use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - GenPkce, ProofKeyCodeExchange, Token, TokenCredentialExecutor, TokenRequest, + GenPkce, ProofKeyCodeExchange, ResponseType, Token, TokenCredentialExecutor, TokenRequest, }; use lazy_static::lazy_static; +use url::Url; use warp::{get, Filter}; static CLIENT_ID: &str = "<CLIENT_ID>"; @@ -16,11 +16,6 @@ lazy_static! { static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap(); } -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct AccessCode { - code: String, -} - // This example shows how to use a code_challenge and code_verifier // to perform the authorization code grant flow with proof key for // code exchange (PKCE). @@ -28,86 +23,32 @@ pub struct AccessCode { // For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow // And the PKCE RFC: https://tools.ietf.org/html/rfc7636 -// Open the default system web browser to the sign in url for authorization. -// This method uses AuthorizationCodeAuthorizationUrl to build the sign in -// url and query needed to get an authorization code and opens the default system -// web browser to this Url. -fn authorization_sign_in() { - let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_scope(vec!["user.read"]) - .with_redirect_uri("http://localhost:8000/redirect") +/// This is the first step in the flow. Build a url that you can use to send an end user +/// to in order to sign in. Then wait for the redirect after sign in to the redirect url +/// you specified in your app. To see a server example listening for the redirect see +/// [Auth Code Grant PKCE Server Example](https://github.com/sreeise/graph-rs-sdk/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs) +fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> Url { + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) .with_pkce(&PKCE) .url() - .unwrap(); - - webbrowser::open(url.as_str()).unwrap(); -} - -// When the authorization code comes in on the redirect from sign in, call the get_credential -// method passing in the authorization code. The AuthorizationCodeCredential can be passed -// to a confidential client application in order to exchange the authorization code -// for an access token. -async fn handle_redirect( - code_option: Option<AccessCode>, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - match code_option { - Some(access_code) => { - // Print out the code for debugging purposes. - println!("{:#?}", access_code.code); - - let authorization_code = access_code.code; - let mut confidential_client = - AuthorizationCodeCredential::builder(CLIENT_ID, CLIENT_SECRET, authorization_code) - .with_redirect_uri("http://localhost:8000/redirect") - .unwrap() - .with_pkce(&PKCE) - .build(); - - // Returns reqwest::Response - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); - - if response.status().is_success() { - let access_token: Token = response.json().await.unwrap(); - - // If all went well here we can print out the OAuth config with the Access Token. - println!("AccessToken: {:#?}", access_token.access_token); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - return Ok(Box::new("Error Logging In! You can close your browser.")); - } - - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } + .unwrap() } -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<AccessCode>() - .map(Some) - .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); - - authorization_sign_in(); - - warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +fn build_confidential_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<String>, +) -> ConfidentialClientApplication<AuthorizationCodeCredential> { + ConfidentialClientApplication::builder(client_id) + .with_auth_code(authorization_code) + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .with_pkce(&PKCE) + .build() } diff --git a/examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs b/examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs deleted file mode 100644 index cb6c2dd1..00000000 --- a/examples/oauth/auth_code_grant/auth_code_grant_refresh_token.rs +++ /dev/null @@ -1,37 +0,0 @@ -use graph_oauth::identity::AuthorizationCodeCredentialBuilder; -use graph_rs_sdk::oauth::{ - AuthorizationCodeCredential, ConfidentialClientApplication, TokenCredentialExecutor, - TokenRequest, -}; - -// Use a refresh token to get a new access token. - -async fn using_auth_code_credential( - credential: &mut AuthorizationCodeCredential, - refresh_token: &str, -) { - credential.with_refresh_token(refresh_token); - - let _response = credential.execute_async().await; -} - -async fn using_confidential_client( - mut credential: AuthorizationCodeCredential, - refresh_token: &str, -) { - credential.with_refresh_token(refresh_token); - let mut confidential_client = ConfidentialClientApplication::from(credential); - - let _response = confidential_client.execute_async().await; -} - -async fn using_auth_code_credential_builder( - credential: AuthorizationCodeCredential, - refresh_token: &str, -) { - let mut credential = AuthorizationCodeCredentialBuilder::from(credential) - .with_refresh_token(refresh_token) - .build(); - - let _response = credential.execute_async().await; -} diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index edb8fe89..88a1ba95 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -1,145 +1,39 @@ use graph_rs_sdk::error::ErrorMessage; -use graph_rs_sdk::oauth::{ - AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - Token, TokenCredentialExecutor, TokenRequest, -}; +use graph_rs_sdk::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; use graph_rs_sdk::*; +use url::Url; use warp::Filter; -// Update these values with your own or provide them directly in the -// methods below. -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; -static SCOPE: &str = "User.Read"; +// Authorization Code Grant Using Client Secret -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct AccessCode { - code: String, -} - -pub fn authorization_sign_in() { - let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI) - .with_scope(vec![SCOPE]) +/// This is the first step in the flow. Build a url that you can use to send an end user +/// to in order to sign in. Then wait for the redirect after sign in to the redirect url +/// you specified in your app. To see a server example listening for the redirect see +/// [Auth Code Grant PKCE Server Example](https://github.com/sreeise/graph-rs-sdk/examples/oauth/auth_code_grant/auth_code_grant_secret.rs) +pub fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> Url { + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_redirect_uri(redirect_uri) + .with_scope(scope) .url() - .unwrap(); - - // web browser crate in dev dependencies will open to default browser in the system. - webbrowser::open(url.as_str()).unwrap(); + .unwrap() } -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<AccessCode>() - .map(Some) - .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); - - authorization_sign_in(); - - warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +async fn auth_code_grant_secret( + authorization_code: &str, + client_id: &str, + client_secret: &str, + scope: Vec<String>, + redirect_uri: &str, +) { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .build(); + + let graph_client = Graph::from(&confidential_client); + + let _response = graph_client.users().list_user().send().await; } - -/// # Use the access code to build Confidential Client Application -/// -/// ```rust -/// use graph_rs_sdk::oauth::ConfidentialClientApplication; -/// -/// // Set the access code and request an access token. -/// // Callers should handle the Result from requesting an access token -/// // in case of an error here. -/// let client_app = ConfidentialClientApplication::builder("client-id") -/// .with_authorization_code("code") -/// .with_client_secret("client-secret") -/// .with_scope(vec!["User.Read"]) -/// .build(); -/// ``` -async fn handle_redirect( - code_option: Option<AccessCode>, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - match code_option { - Some(access_code) => { - // Print out the code for debugging purposes. - println!("{access_code:#?}"); - - let authorization_code = access_code.code; - - // Set the access code and request an access token. - // Callers should handle the Result from requesting an access token - // in case of an error here. - let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_auth_code(authorization_code) - .with_client_secret(CLIENT_SECRET) - .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) - .unwrap() - .build(); - - let client = Graph::from(confidential_client); - let result = client.users().list_user().send().await; - - match result { - Ok(response) => { - println!("{response:#?}"); - - let status = response.status(); - let body: serde_json::Value = response.json().await.unwrap(); - println!("Status: {status:#?}"); - println!("Body: {body:#?}"); - } - Err(err) => { - println!("{err:#?}"); - } - } - - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } -} -/* - let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_auth_code(authorization_code) - .with_client_secret(CLIENT_SECRET) - .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) - .unwrap() - .build(); - - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); - - if response.status().is_success() { - let mut access_token: Token = response.json().await.unwrap(); - - // Enables the printing of the bearer, refresh, and id token. - access_token.enable_pii_logging(true); - println!("{:#?}", access_token); - - // This will print the actual access token to the console. - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<ErrorMessage> = response.json().await; - - match result { - Ok(error_message) => println!("{error_message:#?}"), - Err(err) => println!("Error on deserialization:\n{err:#?}"), - } - } -*/ diff --git a/examples/oauth/auth_code_grant/interactive_authentication.rs b/examples/oauth/auth_code_grant/interactive_authentication.rs index fb0ad837..d547854a 100644 --- a/examples/oauth/auth_code_grant/interactive_authentication.rs +++ b/examples/oauth/auth_code_grant/interactive_authentication.rs @@ -44,7 +44,9 @@ fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCred .interactive_authentication(None) .unwrap(); - confidential_client_builder.with_client_secret(CLIENT_SECRET).build() + confidential_client_builder + .with_client_secret(CLIENT_SECRET) + .build() } async fn authenticate() { diff --git a/examples/oauth/auth_code_grant/mod.rs b/examples/oauth/auth_code_grant/mod.rs index b3d76aaf..1ffebba6 100644 --- a/examples/oauth/auth_code_grant/mod.rs +++ b/examples/oauth/auth_code_grant/mod.rs @@ -1,5 +1,6 @@ pub mod auth_code_grant_pkce; -pub mod auth_code_grant_refresh_token; pub mod auth_code_grant_secret; +pub mod server_examples; + pub mod interactive_authentication; diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs new file mode 100644 index 00000000..e2bcafaa --- /dev/null +++ b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs @@ -0,0 +1,113 @@ +use graph_rs_sdk::error::IdentityResult; +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, ProofKeyCodeExchange, + ResponseType, Token, TokenCredentialExecutor, +}; +use lazy_static::lazy_static; +use warp::{get, Filter}; + +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; + +// You can also pass your own values for PKCE instead of automatic generation by +// calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) +lazy_static! { + static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap(); +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, +} + +// This example shows how to use a code_challenge and code_verifier +// to perform the authorization code grant flow with proof key for +// code exchange (PKCE). +// +// For more info see: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +// And the PKCE RFC: https://tools.ietf.org/html/rfc7636 + +// Open the default system web browser to the sign in url for authorization. +// This method uses AuthorizationCodeAuthorizationUrl to build the sign in +// url and query needed to get an authorization code and opens the default system +// web browser to this Url. +fn authorization_sign_in() { + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_scope(vec!["user.read"]) + .with_redirect_uri("http://localhost:8000/redirect") + .with_pkce(&PKCE) + .url() + .unwrap(); + + webbrowser::open(url.as_str()).unwrap(); +} + +// When the authorization code comes in on the redirect from sign in, call the get_credential +// method passing in the authorization code. The AuthorizationCodeCredential can be passed +// to a confidential client application in order to exchange the authorization code +// for an access token. +async fn handle_redirect( + code_option: Option<AccessCode>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match code_option { + Some(access_code) => { + // Print out the code for debugging purposes. + println!("{:#?}", access_code.code); + + let authorization_code = access_code.code; + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_auth_code(authorization_code) + .with_client_secret(CLIENT_SECRET) + .with_redirect_uri("http://localhost:8000/redirect") + .unwrap() + .with_pkce(&PKCE) + .build(); + + // Returns reqwest::Response + let response = confidential_client.execute_async().await.unwrap(); + println!("{response:#?}"); + + if response.status().is_success() { + let access_token: Token = response.json().await.unwrap(); + + // If all went well here we can print out the OAuth config with the Access Token. + println!("AccessToken: {:#?}", access_token.access_token); + } else { + // See if Microsoft Graph returned an error in the Response body + let result: reqwest::Result<serde_json::Value> = response.json().await; + println!("{result:#?}"); + return Ok(Box::new("Error Logging In! You can close your browser.")); + } + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); + + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); + + authorization_sign_in(); + + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +} diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs new file mode 100644 index 00000000..919148b1 --- /dev/null +++ b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs @@ -0,0 +1,114 @@ +use graph_rs_sdk::error::ErrorMessage; +use graph_rs_sdk::oauth::{ + AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, + Token, TokenCredentialExecutor, +}; +use graph_rs_sdk::*; +use warp::Filter; + +// Update these values with your own or provide them directly in the +// methods below. +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static SCOPE: &str = "User.Read"; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, +} + +pub fn authorization_sign_in() { + let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) + .url() + .unwrap(); + + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); +} + +fn get_graph_client(authorization_code: &str) -> Graph { + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_auth_code(authorization_code) + .with_client_secret(CLIENT_SECRET) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI) + .unwrap() + .build(); + Graph::from(&confidential_client) +} + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); + + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); + + authorization_sign_in(); + + warp::serve(routes).run(([127, 0, 0, 1], 8000)).await; +} + +/// # Use the access code to build Confidential Client Application +/// +/// ```rust +/// use graph_rs_sdk::oauth::ConfidentialClientApplication; +/// +/// // Set the access code and request an access token. +/// // Callers should handle the Result from requesting an access token +/// // in case of an error here. +/// let client_app = ConfidentialClientApplication::builder("client-id") +/// .with_authorization_code("code") +/// .with_client_secret("client-secret") +/// .with_scope(vec!["User.Read"]) +/// .build(); +/// ``` +async fn handle_redirect( + code_option: Option<AccessCode>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match code_option { + Some(access_code) => { + // Print out the code for debugging purposes. + println!("{access_code:#?}"); + + let authorization_code = access_code.code; + let client = get_graph_client(authorization_code.as_str()); + let result = client.users().list_user().send().await; + + match result { + Ok(response) => { + println!("{response:#?}"); + + let status = response.status(); + let body: serde_json::Value = response.json().await.unwrap(); + println!("Status: {status:#?}"); + println!("Body: {body:#?}"); + } + Err(err) => { + println!("{err:#?}"); + } + } + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} diff --git a/examples/oauth/auth_code_grant/server_examples/mod.rs b/examples/oauth/auth_code_grant/server_examples/mod.rs new file mode 100644 index 00000000..7719dfe0 --- /dev/null +++ b/examples/oauth/auth_code_grant/server_examples/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_code_grant_pkce; + +pub mod auth_code_grant_secret; diff --git a/examples/oauth/client_credentials/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/client_credentials_admin_consent.rs index 37dfebd7..79808156 100644 --- a/examples/oauth/client_credentials/client_credentials_admin_consent.rs +++ b/examples/oauth/client_credentials/client_credentials_admin_consent.rs @@ -21,28 +21,25 @@ // or admin. See examples/client_credentials.rs use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::oauth::ClientCredentialsAuthorizationUrlParameters; +use graph_rs_sdk::oauth::ConfidentialClientApplication; use warp::Filter; // The client_id must be changed before running this example. static CLIENT_ID: &str = "<CLIENT_ID>"; -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; -// Paste the URL into a browser and log in to approve the admin consent. -fn get_admin_consent_url() -> IdentityResult<url::Url> { - let authorization_credential = - ClientCredentialsAuthorizationUrlParameters::new(CLIENT_ID, REDIRECT_URI)?; - authorization_credential.url() -} +static TENANT_ID: &str = "<TENANT_ID>"; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // OR use the builder: // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. -fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { - let url_builder = ClientCredentialsAuthorizationUrlParameters::builder(CLIENT_ID) +fn get_admin_consent_url() -> IdentityResult<url::Url> { + let url_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .client_credential_url_builder() .with_redirect_uri(REDIRECT_URI)? .with_state("123") - .with_tenant("tenant_id") + .with_tenant(TENANT_ID) .build(); url_builder.url() } diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index 080801ea..d9123eec 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -13,10 +13,8 @@ mod client_credentials_admin_consent; pub use client_credentials_admin_consent::*; -use graph_rs_sdk::{ - oauth::ClientSecretCredential, oauth::ConfidentialClientApplication, - oauth::TokenCredentialExecutor, Graph, -}; + +use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; // This example shows programmatically getting an access token using the client credentials // flow after admin consent has been granted. If you have not granted admin consent, see @@ -33,20 +31,5 @@ pub async fn get_graph_client() -> Graph { .with_tenant(TENANT_ID) .build(); - Graph::from(confidential_client_application) + Graph::from(&confidential_client_application) } - -/* -pub async fn get_token_silent() { - let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_tenant(TENANT_ID) - .build(); - - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); - - let body: Token = response.json().await.unwrap(); -} - - */ diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 815e4bfb..f03bcdf7 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -20,20 +20,16 @@ mod client_credentials; mod device_code; mod environment_credential; mod is_access_token_expired; -mod openid_connect; +mod openid; -use crate::is_access_token_expired::is_access_token_expired; -use graph_extensions::crypto::GenPkce; use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, - DeviceCodeCredential, ProofKeyCodeExchange, PublicClientApplication, Token, + DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, TokenCredentialExecutor, TokenRequest, }; -fn main() { - is_access_token_expired(); -} +fn main() {} /* // Some examples of what you can use for authentication and getting access tokens. There are diff --git a/examples/oauth/openid/mod.rs b/examples/oauth/openid/mod.rs new file mode 100644 index 00000000..6023e85d --- /dev/null +++ b/examples/oauth/openid/mod.rs @@ -0,0 +1,3 @@ +pub mod openid; + +pub mod server_examples; diff --git a/examples/oauth/openid/openid.rs b/examples/oauth/openid/openid.rs new file mode 100644 index 00000000..41fad7c5 --- /dev/null +++ b/examples/oauth/openid/openid.rs @@ -0,0 +1,43 @@ +use graph_rs_sdk::oauth::{ + ConfidentialClientApplication, IdToken, OpenIdAuthorizationUrlParameters, OpenIdCredential, + Prompt, ResponseMode, ResponseType, Token, TokenCredentialExecutor, TokenRequest, +}; +use graph_rs_sdk::{error::IdentityResult, Graph}; +use url::Url; + +// The client id and client secret must be changed before running this example. +static CLIENT_ID: &str = ""; +static CLIENT_SECRET: &str = ""; +static TENANT_ID: &str = ""; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +// Use the form post response mode when listening on a server instead +// of the URL query because the the query does not get sent to servers. +fn openid_authorization_url() -> IdentityResult<Url> { + Ok(OpenIdCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT_ID) + //.with_default_scope()? + .with_redirect_uri(REDIRECT_URI)? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state("1234") + .with_scope(vec!["User.Read", "User.ReadWrite"]) + .build() + .url()?) +} + +// OpenIdCredential will automatically include the openid scope and therefore +// does not need to be added using with_scope +fn get_graph_client(mut id_token: IdToken) -> Graph { + let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) + .with_openid(id_token.code.unwrap(), CLIENT_SECRET) + .with_tenant(TENANT_ID) + .with_redirect_uri(REDIRECT_URI) + .unwrap() + .with_scope(vec!["User.Read", "User.ReadWrite"]) + .build(); + + Graph::from(&confidential_client) +} diff --git a/examples/oauth/openid/server_examples/mod.rs b/examples/oauth/openid/server_examples/mod.rs new file mode 100644 index 00000000..50ac6f6e --- /dev/null +++ b/examples/oauth/openid/server_examples/mod.rs @@ -0,0 +1 @@ +pub mod openid; diff --git a/examples/oauth/openid_connect/openid_connect_form_post.rs b/examples/oauth/openid/server_examples/openid.rs similarity index 92% rename from examples/oauth/openid_connect/openid_connect_form_post.rs rename to examples/oauth/openid/server_examples/openid.rs index fe3ab525..a3d971b3 100644 --- a/examples/oauth/openid_connect/openid_connect_form_post.rs +++ b/examples/oauth/openid/server_examples/openid.rs @@ -1,9 +1,7 @@ -use graph_oauth::identity::{ - ConfidentialClientApplication, Prompt, ResponseMode, ResponseType, TokenCredentialExecutor, - TokenRequest, +use graph_rs_sdk::oauth::{ + ConfidentialClientApplication, IdToken, OpenIdCredential, Prompt, ResponseMode, ResponseType, + Token, TokenCredentialExecutor, }; -use graph_oauth::oauth::{OpenIdAuthorizationUrlParameters, OpenIdCredential}; -use graph_rs_sdk::oauth::{IdToken, OAuthSerializer, Token}; use tracing_subscriber::fmt::format::FmtSpan; use url::Url; @@ -23,9 +21,6 @@ use url::Url; /// OAuth-enabled applications by using a security token called an ID token. use warp::Filter; -// Use the form post response mode when listening on a server instead -// of the URL query because the the query does not get sent to servers. - // The client id and client secret must be changed before running this example. static CLIENT_ID: &str = ""; static CLIENT_SECRET: &str = ""; @@ -33,6 +28,8 @@ static TENANT_ID: &str = ""; static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +// Use the form post response mode when listening on a server instead +// of the URL query because the the query does not get sent to servers. fn openid_authorization_url() -> anyhow::Result<Url> { Ok(OpenIdCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT_ID) @@ -67,7 +64,7 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let mut access_token: Token = response.json().await.unwrap(); access_token.enable_pii_logging(true); - println!("\n{:#?}\n", access_token); + println!("\n{access_token:#?}\n"); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; diff --git a/examples/oauth/openid_connect/mod.rs b/examples/oauth/openid_connect/mod.rs deleted file mode 100644 index a23e133a..00000000 --- a/examples/oauth/openid_connect/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod openid_connect_form_post; - -pub use openid_connect_form_post::*; diff --git a/examples/oauth_authorization_url/client_credentials.rs b/examples/oauth_authorization_url/client_credentials.rs index 7d82ac45..42377324 100644 --- a/examples/oauth_authorization_url/client_credentials.rs +++ b/examples/oauth_authorization_url/client_credentials.rs @@ -1,3 +1,4 @@ +use graph_oauth::oauth::ClientSecretCredential; use graph_rs_sdk::{error::IdentityResult, oauth::ClientCredentialsAuthorizationUrlParameters}; // The client_id must be changed before running this example. @@ -13,7 +14,7 @@ fn get_admin_consent_url() -> IdentityResult<url::Url> { // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { - let url_builder = ClientCredentialsAuthorizationUrlParameters::builder(CLIENT_ID) + let url_builder = ClientSecretCredential::authorization_url_builder(CLIENT_ID) .with_redirect_uri(REDIRECT_URI)? .with_state("123") .with_tenant("tenant_id") diff --git a/examples/oauth_authorization_url/legacy/implicit_grant.rs b/examples/oauth_authorization_url/legacy/implicit_grant.rs index 897aa595..475f5bae 100644 --- a/examples/oauth_authorization_url/legacy/implicit_grant.rs +++ b/examples/oauth_authorization_url/legacy/implicit_grant.rs @@ -24,6 +24,7 @@ use std::collections::BTreeSet; // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 use graph_rs_sdk::oauth::legacy::ImplicitCredential; use graph_rs_sdk::oauth::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; + fn oauth_implicit_flow() { let authorizer = ImplicitCredential::builder() .with_client_id("<YOUR_CLIENT_ID>") @@ -34,23 +35,22 @@ fn oauth_implicit_flow() { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url = authorizer.url().unwrap(); - // webbrowser crate in dev dependencies will open to default browser in the system. - // Opens the default browser to the Microsoft login page. // After logging in the page will redirect and the Url // will have the access token in either the query or // the fragment of the Uri. + // webbrowser crate in dev dependencies will open to default browser in the system. webbrowser::open(url.as_str()).unwrap(); } fn multi_response_types() { let _ = ImplicitCredential::builder() .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) - .build_credential(); + .build(); // Or @@ -59,5 +59,5 @@ fn multi_response_types() { "token".to_string(), "id_token".to_string(), ]))) - .build_credential(); + .build(); } diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index 37d672bf..769b9401 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -1,5 +1,7 @@ use graph_error::IdentityResult; -use graph_oauth::identity::{ConfidentialClientApplication, OpenIdCredential}; +use graph_rs_sdk::oauth::{ + ConfidentialClientApplication, OpenIdCredential, Prompt, ResponseMode, ResponseType, +}; use url::Url; // The authorization request is the initial request to sign in where the user @@ -7,11 +9,15 @@ use url::Url; // If successful the user will be redirected back to your app and the authorization // code will be in the query of the URL. +// If you are listening on a server use the response mod ResponseMode::FormPost. +// Servers do not get sent the URL query and so in order to get what would normally be in +// the query of URL use a form post which sends the data as a POST http request. + // The URL builder below will create the full URL with the query that you will // need to send the user to such as redirecting the page they are on when using // your app to the URL. -// See examples/oauth/openid_connect for a full example. +// See examples/oauth/openid for a full example. fn open_id_authorization_url( client_id: &str, @@ -42,3 +48,25 @@ fn open_id_authorization_url2( .build() .url() } + +// Use the form post response mode when listening on a server instead +// of the URL query because the the query does not get sent to servers. +fn openid_authorization_url3( + client_id: &str, + tenant: &str, + redirect_uri: &str, + state: &str, + scope: Vec<&str>, +) -> IdentityResult<Url> { + Ok(OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant) + //.with_default_scope()? + .with_redirect_uri(redirect_uri)? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state(state) + .with_scope(scope) + .build() + .url()?) +} diff --git a/examples/paging/README.md b/examples/paging/README.md new file mode 100644 index 00000000..cbd3d180 --- /dev/null +++ b/examples/paging/README.md @@ -0,0 +1,44 @@ +# Paging Additional Information + +For paging, the response bodies are returned in a result, `Result<T, ErrorMessage>` when calling `body()` or `into_body()` +where errors are typically due to deserialization when the Graph Api returns error messages in the `Response` body. +For instance, if you were to call the Graph Api using paging with a custom type and your access token has already +expired the response body will be an error because the response body could not be converted to your custom type. +Because of the way Microsoft Graph returns errors as `Response` bodies, using `serde_json::Value`, for paging +calls will return those errors as `Ok(serde_json::Value)` instead of `Err(ErrorMessage)`. So just keep +this in mind if you do a paging call and specify the body as `serde_json::Value`. + +If you get an unsuccessful status code from the `Response` object you can typically assume +that your response body is an error. With paging, the `Result<T, ErrorMessage>` will include any +Microsoft Graph specific error from the Response body in `ErrorMessage` except when you specify +`serde_json::Value` as the type for `Response` body in the paging call as mentioned above. + +You can however almost always get original response body using `serde_json::Value` from a paging call because +this sdk stores the response in a `serde_json::Value`, transferred in `Response` as `Vec<u8>`, +for each `Response`. To get the original response body as `serde_json::Value` when using custom types, first +add a use statement for `HttpResponseExt`, the sdk trait for `http::Response`: `use graph_rs_sdk::http::HttpResponseExt;` +call the `json` method on the `http::Response<Result<T, ErrorMessage>>` which returns an `Option<serde_json::Value>`. +This `serde_json::Value`, in unsuccessful responses, will almost always be the Microsoft Graph Error. +You can convert this `serde_json::Value` to the provided type, `ErrorMessage`, +from `graph_rs_sdk::error::ErrorMessage`, or to whatever type you choose. + +```rust +use graph_rs_sdk::http::HttpResponseExt; + +fn main() { + // Given response = http::Response<T>> + println!("{:#?}", response.url()); // Get the url of the request. + println!("{:#?}", response.json()); // Get the original JSON that came in the Response +} +``` + +Performance wise, It is better to use `http::Response::body()` and `http::Response::into_body()` for any type, +whether its custom types or `serde_json::Value`, instead of `HttpResponseExt::json()` because +in successful responses the body from `body()` or `into_body()` has already been converted. +The `HttpResponseExt::json` method must convert from `Vec<u8>`. +In general, this method can be used for any use case. However, its provided if needed for debugging and +for error messages that Microsoft Graph returns. + +There are different levels of support for paging Microsoft Graph APIs. See the documentation, +[Paging Microsoft Graph data in your app](https://learn.microsoft.com/en-us/graph/paging), for more info on +supported APIs and availability. diff --git a/examples/next_links/channel.rs b/examples/paging/channel.rs similarity index 100% rename from examples/next_links/channel.rs rename to examples/paging/channel.rs diff --git a/examples/next_links/delta.rs b/examples/paging/delta.rs similarity index 100% rename from examples/next_links/delta.rs rename to examples/paging/delta.rs diff --git a/examples/next_links/main.rs b/examples/paging/main.rs similarity index 100% rename from examples/next_links/main.rs rename to examples/paging/main.rs diff --git a/examples/next_links/stream.rs b/examples/paging/stream.rs similarity index 100% rename from examples/next_links/stream.rs rename to examples/paging/stream.rs diff --git a/examples/paging.rs b/examples/paging_and_next_links.rs similarity index 100% rename from examples/paging.rs rename to examples/paging_and_next_links.rs diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index d1423fca..ca384726 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -10,11 +10,13 @@ description = "Common types and traits for the graph-rs-sdk crate" [dependencies] async-stream = "0.3" async-trait = "0.1.35" +base64 = "0.21.0" dyn-clone = "1.0.14" Inflector = "0.11.4" http = "0.2.9" percent-encoding = "2" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +ring = "0.16.15" serde = { version = "1", features = ["derive"] } serde_json = "1" strum = { version = "0.25.0", features = ["derive"] } diff --git a/graph-extensions/src/cache/in_memory_credential_store.rs b/graph-core/src/cache/in_memory_cache_store.rs similarity index 60% rename from graph-extensions/src/cache/in_memory_credential_store.rs rename to graph-core/src/cache/in_memory_cache_store.rs index 7d69bad7..a4a90bbc 100644 --- a/graph-extensions/src/cache/in_memory_credential_store.rs +++ b/graph-core/src/cache/in_memory_cache_store.rs @@ -1,26 +1,25 @@ -use crate::cache::AsBearer; use std::collections::HashMap; use std::sync::{Arc, RwLock}; #[derive(Clone, Default)] -pub struct InMemoryTokenStore<Token: AsBearer + Clone> { - store: Arc<RwLock<HashMap<String, Token>>>, +pub struct InMemoryCacheStore<Value: Clone> { + store: Arc<RwLock<HashMap<String, Value>>>, } -impl<Token: AsBearer + Clone> InMemoryTokenStore<Token> { - pub fn new() -> InMemoryTokenStore<Token> { - InMemoryTokenStore { +impl<Value: Clone> InMemoryCacheStore<Value> { + pub fn new() -> InMemoryCacheStore<Value> { + InMemoryCacheStore { store: Default::default(), } } - pub fn store<T: Into<String>>(&mut self, cache_id: T, token: Token) { + pub fn store<T: Into<String>>(&mut self, cache_id: T, token: Value) { let mut write_lock = self.store.write().unwrap(); write_lock.insert(cache_id.into(), token); drop(write_lock); } - pub fn get(&self, cache_id: &str) -> Option<Token> { + pub fn get(&self, cache_id: &str) -> Option<Value> { let read_lock = self.store.read().unwrap(); let token = read_lock.get(cache_id).cloned(); drop(read_lock); diff --git a/graph-core/src/cache/mod.rs b/graph-core/src/cache/mod.rs new file mode 100644 index 00000000..1c60c412 --- /dev/null +++ b/graph-core/src/cache/mod.rs @@ -0,0 +1,5 @@ +mod in_memory_cache_store; +mod token_cache; + +pub use in_memory_cache_store::*; +pub use token_cache::*; diff --git a/graph-extensions/src/cache/cache_store.rs b/graph-core/src/cache/token_cache.rs similarity index 95% rename from graph-extensions/src/cache/cache_store.rs rename to graph-core/src/cache/token_cache.rs index 62208ee7..429004a4 100644 --- a/graph-extensions/src/cache/cache_store.rs +++ b/graph-core/src/cache/token_cache.rs @@ -18,7 +18,7 @@ impl AsBearer for &str { } #[async_trait] -pub trait TokenCacheStore { +pub trait TokenCache { type Token: AsBearer; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError>; diff --git a/graph-extensions/src/crypto/mod.rs b/graph-core/src/crypto/mod.rs similarity index 100% rename from graph-extensions/src/crypto/mod.rs rename to graph-core/src/crypto/mod.rs diff --git a/graph-extensions/src/crypto/pkce.rs b/graph-core/src/crypto/pkce.rs similarity index 100% rename from graph-extensions/src/crypto/pkce.rs rename to graph-core/src/crypto/pkce.rs diff --git a/graph-core/src/http/mod.rs b/graph-core/src/http/mod.rs index 13626353..e323e9a4 100644 --- a/graph-core/src/http/mod.rs +++ b/graph-core/src/http/mod.rs @@ -1,3 +1,5 @@ mod response_builder_ext; +mod response_converter; pub use response_builder_ext::*; +pub use response_converter::*; diff --git a/graph-extensions/src/http/response_converter.rs b/graph-core/src/http/response_converter.rs similarity index 100% rename from graph-extensions/src/http/response_converter.rs rename to graph-core/src/http/response_converter.rs diff --git a/graph-core/src/identity/client_application.rs b/graph-core/src/identity/client_application.rs index a0eb4193..bbe015f0 100644 --- a/graph-core/src/identity/client_application.rs +++ b/graph-core/src/identity/client_application.rs @@ -5,8 +5,19 @@ use graph_error::AuthExecutionResult; dyn_clone::clone_trait_object!(ClientApplication); #[async_trait] -pub trait ClientApplication: DynClone { +pub trait ClientApplication: DynClone + Send + Sync { fn get_token_silent(&mut self) -> AuthExecutionResult<String>; async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; } + +#[async_trait] +impl ClientApplication for String { + fn get_token_silent(&mut self) -> AuthExecutionResult<String> { + Ok(self.clone()) + } + + async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { + Ok(self.clone()) + } +} diff --git a/graph-core/src/lib.rs b/graph-core/src/lib.rs index 9d423a40..da5cf5ad 100644 --- a/graph-core/src/lib.rs +++ b/graph-core/src/lib.rs @@ -6,6 +6,8 @@ extern crate strum; #[macro_use] extern crate serde; +pub mod cache; +pub mod crypto; pub mod http; pub mod identity; pub mod resource; diff --git a/graph-extensions/Cargo.toml b/graph-extensions/Cargo.toml deleted file mode 100644 index 281e87a7..00000000 --- a/graph-extensions/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "graph-extensions" -version = "0.1.0" -edition = "2021" -license = "MIT" -repository = "https://github.com/sreeise/graph-rs-sdk" -description = "Extensions and utilities used across multiple crates that make up the graph-rs-sdk crate" - -[dependencies] -anyhow = { version = "1.0.69", features = ["backtrace"]} -async-stream = "0.3" -async-trait = "0.1.35" -base64 = "0.21.0" -bytes = { version = "1.4.0", features = ["serde"] } -chrono = { version = "0.4.23", features = ["serde"] } -chrono-humanize = "0.2.2" -dyn-clone = "1.0.14" -futures = "0.3.28" -http = "0.2.9" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } -ring = "0.16.20" -serde-aux = "4.1.2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_urlencoded = "0.7.1" -time = { version = "0.3.10", features = ["local-offset", "serde"] } -tokio = { version = "1.27.0", features = ["full"] } -url = { version = "2", features = ["serde"] } - -graph-error = { path = "../graph-error" } - -[features] -default = ["native-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] -brotli = ["reqwest/brotli"] -deflate = ["reqwest/deflate"] -trust-dns = ["reqwest/trust-dns"] diff --git a/graph-extensions/src/cache/mod.rs b/graph-extensions/src/cache/mod.rs deleted file mode 100644 index a306a091..00000000 --- a/graph-extensions/src/cache/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod cache_store; -mod in_memory_credential_store; - -pub use cache_store::*; -pub use in_memory_credential_store::*; diff --git a/graph-extensions/src/http/mod.rs b/graph-extensions/src/http/mod.rs deleted file mode 100644 index e323e9a4..00000000 --- a/graph-extensions/src/http/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod response_builder_ext; -mod response_converter; - -pub use response_builder_ext::*; -pub use response_converter::*; diff --git a/graph-extensions/src/http/response_builder_ext.rs b/graph-extensions/src/http/response_builder_ext.rs deleted file mode 100644 index 2c9dadbc..00000000 --- a/graph-extensions/src/http/response_builder_ext.rs +++ /dev/null @@ -1,53 +0,0 @@ -use url::Url; - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtUrl(pub Url); - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtSerdeJsonValue(pub serde_json::Value); - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct HttpExtVecU8(pub Vec<u8>); - -/// Extension trait for http::response::Builder objects -/// -/// Allows the user to add a `Url` to the http::Response -pub trait HttpResponseBuilderExt { - /// A builder method for the `http::response::Builder` type that allows the user to add a `Url` - /// to the `http::Response` - fn url(self, url: Url) -> Self; - fn json(self, value: &serde_json::Value) -> Self; -} - -impl HttpResponseBuilderExt for http::response::Builder { - fn url(self, url: Url) -> Self { - self.extension(HttpExtUrl(url)) - } - - fn json(self, value: &serde_json::Value) -> Self { - if let Ok(value) = serde_json::to_vec(value) { - return self.extension(HttpExtVecU8(value)); - } - - self - } -} - -pub trait HttpResponseExt { - fn url(&self) -> Option<Url>; - fn json(&self) -> Option<serde_json::Value>; -} - -impl<T> HttpResponseExt for http::Response<T> { - fn url(&self) -> Option<Url> { - self.extensions() - .get::<HttpExtUrl>() - .map(|url| url.clone().0) - } - - fn json(&self) -> Option<serde_json::Value> { - self.extensions() - .get::<HttpExtVecU8>() - .and_then(|value| serde_json::from_slice(value.0.as_slice()).ok()) - } -} diff --git a/graph-extensions/src/lib.rs b/graph-extensions/src/lib.rs deleted file mode 100644 index 8b890f74..00000000 --- a/graph-extensions/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod cache; -pub mod crypto; -pub mod http; diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 1e57f21c..55e1b4b0 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -24,15 +24,12 @@ tokio = { version = "1.27.0", features = ["full"] } url = { version = "2", features = ["serde"] } graph-error = { path = "../graph-error" } -graph-core = { path = "../graph-core" } -graph-extensions = { path = "../graph-extensions" } -graph-oauth = { path = "../graph-oauth", version = "1.0.2", default-features=false } +graph-core = { path = "../graph-core", default-features = false } [features] default = ["native-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] -brotli = ["reqwest/brotli"] -deflate = ["reqwest/deflate"] -trust-dns = ["reqwest/trust-dns"] -openssl = ["graph-oauth/openssl"] +native-tls = ["reqwest/native-tls", "graph-core/native-tls"] +rustls-tls = ["reqwest/rustls-tls", "graph-core/rustls-tls"] +brotli = ["reqwest/brotli", "graph-core/brotli"] +deflate = ["reqwest/deflate", "graph-core/deflate"] +trust-dns = ["reqwest/trust-dns", "graph-core/trust-dns"] diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index d7436cbb..9fd1f923 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,10 +1,5 @@ use crate::blocking::BlockingClient; use graph_core::identity::ClientApplication; -use graph_extensions::cache::TokenCacheStore; -use graph_oauth::identity::{ - BearerTokenCredential, ConfidentialClientApplication, PublicClientApplication, - TokenCredentialExecutor, -}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -80,9 +75,7 @@ impl GraphClientConfiguration { } pub fn access_token<AT: ToString>(mut self, access_token: AT) -> GraphClientConfiguration { - self.config.client_application = Some(Box::new(BearerTokenCredential::new( - access_token.to_string(), - ))); + self.config.client_application = Some(Box::new(access_token.to_string())); self } @@ -91,25 +84,27 @@ impl GraphClientConfiguration { self } - pub fn confidential_client_application< - Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, - >( - mut self, - confidential_client: ConfidentialClientApplication<Credential>, - ) -> Self { - self.config.client_application = Some(Box::new(confidential_client)); - self - } - - pub fn public_client_application< - Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, - >( - mut self, - public_client: PublicClientApplication<Credential>, - ) -> Self { - self.config.client_application = Some(Box::new(public_client)); - self - } + /* + pub fn confidential_client_application< + Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, + >( + mut self, + confidential_client: ConfidentialClientApplication<Credential>, + ) -> Self { + self.config.client_application = Some(Box::new(confidential_client)); + self + } + + pub fn public_client_application< + Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, + >( + mut self, + public_client: PublicClientApplication<Credential>, + ) -> Self { + self.config.client_application = Some(Box::new(public_client)); + self + } + */ pub fn default_headers(mut self, headers: HeaderMap) -> GraphClientConfiguration { for (key, value) in headers.iter() { @@ -199,7 +194,7 @@ impl GraphClientConfiguration { } } else { Client { - client_application: Box::new(BearerTokenCredential::new(String::default())), + client_application: Box::new(String::default()), inner: builder.build().unwrap(), headers, builder: config, @@ -233,7 +228,7 @@ impl GraphClientConfiguration { } } else { BlockingClient { - client_application: Box::new(BearerTokenCredential::new(String::default())), + client_application: Box::new(String::default()), inner: builder.build().unwrap(), headers, } @@ -301,6 +296,7 @@ impl Debug for Client { } } +/* impl From<BearerTokenCredential> for Client { fn from(value: BearerTokenCredential) -> Self { Client::new(value) @@ -322,6 +318,7 @@ impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'sta Client::new(value) } } + */ #[cfg(test)] mod test { diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 3b6ec102..e28e37c3 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -28,7 +28,7 @@ pub(crate) mod internal { pub use crate::traits::*; pub use crate::upload_session::*; pub use crate::url::*; - pub use graph_extensions::http::*; + pub use graph_core::http::*; } pub mod api_impl { diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 40c8a9fe..3622079b 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -26,16 +26,15 @@ hex = "0.4.3" http = "0.2.9" openssl = { version = "0.10", optional=true } reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } -ring = "0.16.15" serde = { version = "1", features = ["derive"] } serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.24.1", features = ["derive"] } url = { version = "2", features = ["serde"] } -time = { version = "0.3.10", features = ["local-offset"] } +time = { version = "0.3.10", features = ["local-offset", "serde"] } webbrowser = "0.8.7" -wry = { version = "0.33.1", optional = true} +wry = { version = "0.33.1"} uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } hyper = { version = "1.0.0-rc.3", features = ["full"] } @@ -43,18 +42,17 @@ http-body-util = "0.1.0-rc.2" tracing = "0.1.37" graph-error = { path = "../graph-error" } -graph-extensions = { path = "../graph-extensions" } -graph-core = { path = "../graph-core" } +graph-core = { path = "../graph-core", default-features = false } [features] default = ["native-tls"] -native-tls = ["reqwest/native-tls"] -rustls-tls = ["reqwest/rustls-tls"] -brotli = ["reqwest/brotli"] -deflate = ["reqwest/deflate"] -trust-dns = ["reqwest/trust-dns"] +native-tls = ["reqwest/native-tls", "graph-core/native-tls"] +rustls-tls = ["reqwest/rustls-tls", "graph-core/rustls-tls"] +brotli = ["reqwest/brotli", "graph-core/brotli"] +deflate = ["reqwest/deflate", "graph-core/deflate"] +trust-dns = ["reqwest/trust-dns", "graph-core/trust-dns"] openssl = ["dep:openssl"] -interactive-auth = ["dep:wry"] +# interactive-auth = ["dep:wry"] [[test]] name = "x509_certificate_tests" diff --git a/graph-oauth/README.md b/graph-oauth/README.md index 20a37859..dc6286c4 100644 --- a/graph-oauth/README.md +++ b/graph-oauth/README.md @@ -1,4 +1,11 @@ -# OAuth client implementing the OAuth 2.0 and OpenID Connect protocols for Microsoft identity platform +# OAuth 2.0 and OpenID Connect Client For The Microsoft Identity Platform + +Support for: + +- Automatic Token Refresh +- Interactive Authentication | features = [`interactive-auth`] +- Device Code Polling +- Authorization Using Certificates | features = [`openssl`] Purpose built as OAuth client for Microsoft Graph and the [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) project. This project can however be used outside [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) as an OAuth client @@ -21,86 +28,219 @@ graph-oauth = "1.0.2" - `native-tls`: Use the `native-tls` TLS backend (OpenSSL on *nix, SChannel on Windows, Secure Transport on macOS). - `rustls-tls`: Use the `rustls-tls` TLS backend (cross-platform backend, only supports TLS 1.2 and 1.3). +- `interactive-auth`: Interactive Authentication using the [wry](https://github.com/tauri-apps/wry) crate to run web view on + platforms that support it such as on a desktop. +- `openssl`: Use X509 Certificates from the openssl crate in the OAuth2 and OpenId Connect flows. Default features: `default=["native-tls"]` These features enable the native-tls and rustls-tls features in the reqwest crate. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -### Supported Authorization Flows -#### Microsoft OneDrive and SharePoint -- [Token Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#token-flow) -- [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) +## OAuth - Getting Access Tokens + +The crate is undergoing major development in order to support all or most scenarios in the +Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some +unstable features. Any version that is not a pre-release version of the crate is considered stable. + +Use application builders to store your auth configuration and have the client +handle the access token requests for you. + +There are two main types for building your chosen OAuth or OpenId Connect Flow. + +- `PublicClientApplication` +- `ConfidentialClientApplication` + +Once you have built a `ConfidentialClientApplication` or a `PublicClientApplication` +you can pass these to the graph client. + +Automatic token refresh is also done by passing the `ConfidentialClientApplication` or the +`PublicClientApplication` to the `Graph` client. + +For more extensive examples see the +[OAuth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth) in the examples/oauth +directory on [GitHub](https://github.com/sreeise/graph-rs-sdk). + + +```rust,ignore +let confidental_client: ConfidentialClientApplication<ClientSecretCredential> = ... + +let graph_client = Graph::from(confidential_client); +``` + +### Identity Platform Support -#### Microsoft Identity Platform +The following flows from the Microsoft Identity Platform are supported: - [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) - [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Authorization Code Grant Certificate](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) -- [Implicit Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) - [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) -For more extensive examples and explanations see the -[OAuth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth) in the examples/oauth -directory on [GitHub](https://github.com/sreeise/graph-rs-sdk). +You can use the url builders for those flows that require an authorization code using a redirect after sign in you can use + +### Examples + +### Authorization Code Grant + +The authorization code grant is considered a confidential client (except in the hybrid flow) +and we can get an access token by using the authorization code returned in the query of the URL +on redirect after sign in is performed by the user. + +Once you have the authorization code you can pass this to the client and the client +will perform the request to get an access token on the first graph api call that you make. ```rust -use graph_oauth::oauth::{AccessToken, OAuth}; - -fn main() { - let mut oauth = OAuth::new(); - oauth - .client_id("<YOUR_CLIENT_ID>") - .client_secret("<YOUR_CLIENT_SECRET>") - .add_scope("files.read") - .add_scope("files.readwrite") - .add_scope("files.read.all") - .add_scope("files.readwrite.all") - .add_scope("offline_access") - .redirect_uri("http://localhost:8000/redirect") - .authorize_url("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") - .access_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .refresh_token_url("https://login.microsoftonline.com/common/oauth2/v2.0/token") - .response_type("code") - .logout_url("https://login.microsoftonline.com/common/oauth2/v2.0/logout") - .post_logout_redirect_uri("http://localhost:8000/redirect"); - - let mut request = oauth.build().authorization_code_grant(); - - // Opens the default browser. - let _ = request.browser_authorization().open(); - - // The access code will be appended to the url on redirect. Pass - // this code to the OAuth instance: - oauth.access_code("<ACCESS CODE>"); - - // Perform an authorization code grant request for an access token: - let response = request.access_token().send().await?; - println!("{response:#?}"); +use graph_rs_sdk::{ + Graph, + oauth::ConfidentialClientApplication, +}; + +#[tokio::main] +async fn main() { + let authorization_code = "<AUTH_CODE>"; + let client_id = "<CLIENT_ID>"; + let client_secret = "<CLIENT_SECRET>"; + let scope = vec!["<SCOPE>", "<SCOPE>"]; + let redirect_uri = "http://localhost:8080"; + + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .build(); + + let graph_client = Graph::from(confidential_client); + + let _response = graph_client + .users() + .list_user() + .send() // Also makes first access token request at this point + .await; +} +``` - if response.status().is_success() { - let mut access_token: AccessToken = response.json().await?; +### Client Credentials Grant. - // Option<&JsonWebToken> - let jwt = access_token.jwt(); - println!("{jwt:#?}"); +The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, +instead of impersonating a user, to authenticate when calling another web service. The grant specified in RFC 6749, +sometimes called two-legged OAuth, can be used to access web-hosted resources by using the identity of an application. +This type is commonly used for server-to-server interactions that must run in the background, without immediate +interaction with a user, and is often referred to as daemons or service accounts. + +Client credentials flow requires a one time administrator acceptance +of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions +as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) + +```rust +use graph_rs_sdk::{ + oauth::ConfidentialClientApplication, Graph +}; - oauth.access_token(access_token); +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static TENANT_ID: &str = "<TENANT_ID>"; - // If all went well here we can print out the OAuth config with the Access Token. - println!("{:#?}", &oauth); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; +pub async fn get_graph_client() -> Graph { + let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) + .build(); - match result { - Ok(body) => println!("{body:#?}"), - Err(err) => println!("Error on deserialization:\n{err:#?}"), - } - } + Graph::from(confidential_client_application) +} +``` + + +### Automatic Token Refresh + +Using automatic token refresh requires getting a refresh token as part of the token response. +To get a refresh token you must include the `offline_access` scope. + +Automatic token refresh is done by passing the `ConfidentialClientApplication` or the +`PublicClientApplication` to the `Graph` client. + +If you are using the `client credentials` grant you do not need the `offline_access` scope. +Tokens will still be automatically refreshed as this flow does not require using a refresh token to get +a new access token. + +```rust +async fn authenticate() { + let scope = vec!["offline_access"]; + let mut credential_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .auth_code_url_builder() + .interactive_authentication(None) // Open web view for interactive authentication sign in + .unwrap(); + // ... add any other parameters you need + + let confidential_client = credential_builder.with_client_secret(CLIENT_SECRET) + .build(); + + let client = Graph::from(&confidential_client); +} +``` + + +### Interactive Authentication + +Requires Feature `interactive_auth` + +```toml +[dependencies] +graph-rs-sdk = { version = "...", features = ["interactive_auth"] } +``` + +Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) crate to run web view on +platforms that support it such as on a desktop. + +```rust +use graph_rs_sdk::oauth::{ + web::Theme, web::WebViewOptions, AuthorizationCodeCredential, + ConfidentialClientApplication +}; +use graph_rs_sdk::Graph; + +fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { + let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) + .auth_code_url_builder() + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .interactive_authentication(None) + .unwrap(); + + confidential_client_builder.with_client_secret(CLIENT_SECRET).build() +} + +async fn authenticate() { + // Create a tracing subscriber to log debug/trace events coming from + // authorization http calls and the Graph client. + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::TRACE) + .init(); + + let mut confidential_client = run_interactive_auth(); + + let client = Graph::from(&confidential_client); + + let response = client.user(USER_ID) + .get_user() + .send() + .await + .unwrap(); + + println!("{response:#?}"); + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); } ``` diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 41c8d3a6..e94f9c4c 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -4,7 +4,6 @@ use std::default::Default; use std::fmt; use base64::Engine; -use ring::rand::SecureRandom; use url::form_urlencoded::Serializer; use url::Url; @@ -46,8 +45,6 @@ pub enum OAuthParameter { CodeVerifier, CodeChallenge, CodeChallengeMethod, - PostLogoutRedirectURI, - LogoutURL, AdminConsent, Username, Password, @@ -83,8 +80,6 @@ impl OAuthParameter { OAuthParameter::CodeVerifier => "code_verifier", OAuthParameter::CodeChallenge => "code_challenge", OAuthParameter::CodeChallengeMethod => "code_challenge_method", - OAuthParameter::LogoutURL => "logout_url", - OAuthParameter::PostLogoutRedirectURI => "post_logout_redirect_uri", OAuthParameter::AdminConsent => "admin_consent", OAuthParameter::Username => "username", OAuthParameter::Password => "password", @@ -126,13 +121,14 @@ impl AsRef<str> for OAuthParameter { /// /// # Example /// ``` -/// use graph_oauth::oauth::OAuthSerializer; +/// use graph_oauth::extensions::OAuthSerializer; /// let oauth = OAuthSerializer::new(); /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct OAuthSerializer { scopes: BTreeSet<String>, - credentials: BTreeMap<String, String>, + parameters: BTreeMap<String, String>, + log_pii: bool, } impl OAuthSerializer { @@ -140,14 +136,15 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// use graph_oauth::oauth::OAuthSerializer; + /// use graph_oauth::extensions::OAuthSerializer; /// /// let mut oauth = OAuthSerializer::new(); /// ``` pub fn new() -> OAuthSerializer { OAuthSerializer { scopes: BTreeSet::new(), - credentials: BTreeMap::new(), + parameters: BTreeMap::new(), + log_pii: false, } } @@ -158,8 +155,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// oauth.insert(OAuthParameter::AuthorizationUrl, "https://example.com"); /// assert!(oauth.contains(OAuthParameter::AuthorizationUrl)); @@ -168,10 +165,7 @@ impl OAuthSerializer { pub fn insert<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut OAuthSerializer { let v = value.to_string(); match oac { - OAuthParameter::PostLogoutRedirectURI - | OAuthParameter::TokenUrl - | OAuthParameter::AuthorizationUrl - | OAuthParameter::LogoutURL => { + OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl => { Url::parse(v.as_ref()) .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) .unwrap(); @@ -179,7 +173,7 @@ impl OAuthSerializer { _ => {} } - self.credentials.insert(oac.to_string(), v); + self.parameters.insert(oac.to_string(), v); self } @@ -189,8 +183,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// let entry = oauth.entry_with(OAuthParameter::AuthorizationUrl, "https://example.com"); /// assert_eq!(entry, "https://example.com") @@ -198,10 +192,7 @@ impl OAuthSerializer { pub fn entry_with<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { let v = value.to_string(); match oac { - OAuthParameter::PostLogoutRedirectURI - | OAuthParameter::TokenUrl - | OAuthParameter::AuthorizationUrl - | OAuthParameter::LogoutURL => { + OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl => { Url::parse(v.as_ref()) .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) .unwrap(); @@ -209,7 +200,7 @@ impl OAuthSerializer { _ => {} } - self.credentials + self.parameters .entry(oac.alias().to_string()) .or_insert_with(|| v) } @@ -220,28 +211,28 @@ impl OAuthSerializer { /// /// [`entry`]: BTreeMap::entry pub fn entry<V: ToString>(&mut self, oac: OAuthParameter) -> Entry<String, String> { - self.credentials.entry(oac.alias().to_string()) + self.parameters.entry(oac.alias().to_string()) } /// Get a previously set credential. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// let a = oauth.get(OAuthParameter::AuthorizationUrl); /// ``` pub fn get(&self, oac: OAuthParameter) -> Option<String> { - self.credentials.get(oac.alias()).cloned() + self.parameters.get(oac.alias()).cloned() } /// Check if an OAuth credential has already been set. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// println!("{:#?}", oauth.contains(OAuthParameter::Nonce)); /// ``` @@ -249,19 +240,19 @@ impl OAuthSerializer { if t == OAuthParameter::Scope { return !self.scopes.is_empty(); } - self.credentials.contains_key(t.alias()) + self.parameters.contains_key(t.alias()) } pub fn contains_key(&self, key: &str) -> bool { - self.credentials.contains_key(key) + self.parameters.contains_key(key) } /// Remove a field from OAuth. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_id("client_id"); /// @@ -271,7 +262,7 @@ impl OAuthSerializer { /// assert_eq!(oauth.contains(OAuthParameter::ClientId), false); /// ``` pub fn remove(&mut self, oac: OAuthParameter) -> &mut OAuthSerializer { - self.credentials.remove(oac.alias()); + self.parameters.remove(oac.alias()); self } @@ -279,8 +270,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_id("client_id"); /// ``` @@ -292,8 +283,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # use graph_oauth::oauth::OAuthParameter; + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); /// oauth.state("1234"); /// ``` @@ -305,7 +296,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_secret("client_secret"); /// ``` @@ -317,7 +308,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.authorization_url("https://example.com/authorize"); /// ``` @@ -329,7 +320,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.token_uri("https://example.com/token"); /// ``` @@ -341,7 +332,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.refresh_token_url("https://example.com/token"); /// ``` @@ -353,8 +344,8 @@ impl OAuthSerializer { /// for OAuth based on a tenant id. /// /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// ```rust + /// # use crate::graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.tenant_id("tenant_id"); /// ``` @@ -370,7 +361,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.tenant_id("tenant_id"); /// ``` @@ -425,7 +416,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.redirect_uri("https://localhost:8888/redirect"); /// ``` @@ -437,9 +428,9 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.authorization_code("LDSF[POK43"); + /// # use graph_oauth::extensions::OAuthSerializer; + /// # let mut serializer = OAuthSerializer::new(); + /// serializer.authorization_code("LDSF[POK43"); /// ``` pub fn authorization_code(&mut self, value: &str) -> &mut OAuthSerializer { self.insert(OAuthParameter::AuthorizationCode, value) @@ -449,9 +440,9 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.response_mode("query"); + /// # use graph_oauth::extensions::OAuthSerializer; + /// # let mut serializer = OAuthSerializer::new(); + /// serializer.response_mode("query"); /// ``` pub fn response_mode(&mut self, value: &str) -> &mut OAuthSerializer { self.insert(OAuthParameter::ResponseMode, value) @@ -461,7 +452,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.response_type("token"); /// ``` @@ -480,7 +471,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// /// # let mut oauth = OAuthSerializer::new(); /// oauth.nonce("1234"); @@ -493,7 +484,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// /// # let mut oauth = OAuthSerializer::new(); /// oauth.prompt("login"); @@ -510,7 +501,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.session_state("session-state"); /// ``` @@ -522,7 +513,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.grant_type("token"); /// ``` @@ -534,7 +525,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.resource("resource"); /// ``` @@ -546,7 +537,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_verifier("code_verifier"); /// ``` @@ -558,7 +549,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.domain_hint("domain_hint"); /// ``` @@ -570,7 +561,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_challenge("code_challenge"); /// ``` @@ -582,7 +573,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.code_challenge_method("code_challenge_method"); /// ``` @@ -590,67 +581,11 @@ impl OAuthSerializer { self.insert(OAuthParameter::CodeChallengeMethod, value) } - /// Generate a code challenge and code verifier for the - /// authorization code grant flow using proof key for - /// code exchange (PKCE) and SHA256. - /// - /// This method automatically sets the code_verifier, - /// code_challenge, and code_challenge_method fields. - /// - /// For authorization, the code_challenge_method parameter in the request body - /// is automatically set to 'S256'. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - /// - /// - /// For more info on PKCE and entropy see: <https://tools.ietf.org/html/rfc7519#section-7.2> - /// - /// # Example - /// ``` - /// # use base64::Engine; - /// use graph_oauth::oauth::OAuthSerializer; - /// use graph_oauth::oauth::OAuthParameter; - /// - /// let mut oauth = OAuthSerializer::new(); - /// oauth.generate_sha256_challenge_and_verifier().unwrap(); - /// - /// # assert!(oauth.contains(OAuthParameter::CodeChallenge)); - /// # assert!(oauth.contains(OAuthParameter::CodeVerifier)); - /// # assert!(oauth.contains(OAuthParameter::CodeChallengeMethod)); - /// println!("Code Challenge: {:#?}", oauth.get(OAuthParameter::CodeChallenge)); - /// println!("Code Verifier: {:#?}", oauth.get(OAuthParameter::CodeVerifier)); - /// println!("Code Challenge Method: {:#?}", oauth.get(OAuthParameter::CodeChallengeMethod)); - /// - /// # let challenge = oauth.get(OAuthParameter::CodeChallenge).unwrap(); - /// # let mut context = ring::digest::Context::new(&ring::digest::SHA256); - /// # context.update(oauth.get(OAuthParameter::CodeVerifier).unwrap().as_bytes()); - /// # let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - /// # assert_eq!(challenge, verifier); - /// ``` - pub fn generate_sha256_challenge_and_verifier(&mut self) -> Result<(), GraphFailure> { - let mut buf = [0; 32]; - let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf)?; - let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf); - let mut context = ring::digest::Context::new(&ring::digest::SHA256); - context.update(verifier.as_bytes()); - let code_challenge = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(context.finish().as_ref()); - - self.code_verifier(&verifier); - self.code_challenge(&code_challenge); - self.code_challenge_method("S256"); - Ok(()) - } - /// Set the login hint. /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.login_hint("login_hint"); /// ``` @@ -662,7 +597,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_assertion("client_assertion"); /// ``` @@ -674,7 +609,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// oauth.client_assertion_type("client_assertion_type"); /// ``` @@ -682,35 +617,11 @@ impl OAuthSerializer { self.insert(OAuthParameter::ClientAssertionType, value) } - /// Set the url to send a post request that will log out the user. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.logout_url("https://example.com/logout?"); - /// ``` - pub fn logout_url(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::LogoutURL, value) - } - - /// Set the redirect uri that user will be redirected to after logging out. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.post_logout_redirect_uri("http://localhost:8080"); - /// ``` - pub fn post_logout_redirect_uri(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::PostLogoutRedirectURI, value) - } - /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; /// # let mut oauth = OAuthSerializer::new(); /// oauth.username("user"); /// assert!(oauth.contains(OAuthParameter::Username)) @@ -723,7 +634,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; /// # let mut oauth = OAuthSerializer::new(); /// oauth.password("user"); /// assert!(oauth.contains(OAuthParameter::Password)) @@ -740,7 +651,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; + /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; /// # let mut oauth = OAuthSerializer::new(); /// oauth.device_code("device_code"); /// assert!(oauth.contains(OAuthParameter::DeviceCode)) @@ -753,7 +664,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Sites.Read") @@ -770,7 +681,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// let mut oauth = OAuthSerializer::new(); /// oauth.add_scope("Files.Read"); /// oauth.add_scope("Files.ReadWrite"); @@ -787,7 +698,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// /// // the scopes take a separator just like Vec join. @@ -806,7 +717,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # use std::collections::HashSet; /// # let mut oauth = OAuthSerializer::new(); /// @@ -824,7 +735,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Files.Read"); @@ -842,7 +753,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("scope"); @@ -858,7 +769,7 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::oauth::OAuthSerializer; + /// # use graph_oauth::extensions::OAuthSerializer; /// # let mut oauth = OAuthSerializer::new(); /// /// oauth.add_scope("Files.Read").add_scope("Files.ReadWrite"); @@ -981,7 +892,7 @@ impl OAuthSerializer { /// /// # Example /// ``` -/// # use graph_oauth::oauth::{OAuthSerializer, OAuthParameter}; +/// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; /// # use std::collections::HashMap; /// # let mut oauth = OAuthSerializer::new(); /// let mut map: HashMap<OAuthParameter, &str> = HashMap::new(); @@ -1003,8 +914,10 @@ impl<V: ToString> Extend<(OAuthParameter, V)> for OAuthSerializer { impl fmt::Debug for OAuthSerializer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut map_debug: BTreeMap<&str, &str> = BTreeMap::new(); - for (key, value) in self.credentials.iter() { - if let Some(oac) = OAuthParameter::iter() + for (key, value) in self.parameters.iter() { + if self.log_pii { + map_debug.insert(key.as_str(), value.as_str()); + } else if let Some(oac) = OAuthParameter::iter() .find(|oac| oac.alias().eq(key.as_str()) && oac.is_debug_redacted()) { map_debug.insert(oac.alias(), "[REDACTED]"); @@ -1013,10 +926,164 @@ impl fmt::Debug for OAuthSerializer { } } - f.debug_struct("AccessToken") - .field("access_token", &"[REDACTED]".to_string()) + f.debug_struct("OAuthSerializer") .field("credentials", &map_debug) .field("scopes", &self.scopes) .finish() } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn oauth_parameters_from_credential() { + // Doesn't matter the flow here as this is for testing + // that the credentials are entered/retrieved correctly. + let mut oauth = OAuthSerializer::new(); + oauth + .client_id("client_id") + .client_secret("client_secret") + .authorization_url("https://example.com/authorize?") + .token_uri("https://example.com/token?") + .refresh_token_url("https://example.com/token?") + .redirect_uri("https://example.com/redirect?") + .authorization_code("ADSLFJL4L3") + .response_mode("response_mode") + .response_type("response_type") + .state("state") + .grant_type("grant_type") + .nonce("nonce") + .prompt("login") + .session_state("session_state") + .client_assertion("client_assertion") + .client_assertion_type("client_assertion_type") + .code_verifier("code_verifier") + .login_hint("login_hint") + .domain_hint("domain_hint") + .resource("resource"); + + OAuthParameter::iter().for_each(|credential| { + if oauth.contains(credential) { + match credential { + OAuthParameter::ClientId => { + assert_eq!(oauth.get(credential), Some("client_id".into())) + } + OAuthParameter::ClientSecret => { + assert_eq!(oauth.get(credential), Some("client_secret".into())) + } + OAuthParameter::AuthorizationUrl => assert_eq!( + oauth.get(credential), + Some("https://example.com/authorize?".into()) + ), + OAuthParameter::TokenUrl => assert_eq!( + oauth.get(credential), + Some("https://example.com/token?".into()) + ), + OAuthParameter::RefreshTokenUrl => assert_eq!( + oauth.get(credential), + Some("https://example.com/token?".into()) + ), + OAuthParameter::RedirectUri => assert_eq!( + oauth.get(credential), + Some("https://example.com/redirect?".into()) + ), + OAuthParameter::AuthorizationCode => { + assert_eq!(oauth.get(credential), Some("ADSLFJL4L3".into())) + } + OAuthParameter::ResponseMode => { + assert_eq!(oauth.get(credential), Some("response_mode".into())) + } + OAuthParameter::ResponseType => { + assert_eq!(oauth.get(credential), Some("response_type".into())) + } + OAuthParameter::State => { + assert_eq!(oauth.get(credential), Some("state".into())) + } + OAuthParameter::GrantType => { + assert_eq!(oauth.get(credential), Some("grant_type".into())) + } + OAuthParameter::Nonce => { + assert_eq!(oauth.get(credential), Some("nonce".into())) + } + OAuthParameter::Prompt => { + assert_eq!(oauth.get(credential), Some("login".into())) + } + OAuthParameter::SessionState => { + assert_eq!(oauth.get(credential), Some("session_state".into())) + } + OAuthParameter::ClientAssertion => { + assert_eq!(oauth.get(credential), Some("client_assertion".into())) + } + OAuthParameter::ClientAssertionType => { + assert_eq!(oauth.get(credential), Some("client_assertion_type".into())) + } + OAuthParameter::CodeVerifier => { + assert_eq!(oauth.get(credential), Some("code_verifier".into())) + } + OAuthParameter::Resource => { + assert_eq!(oauth.get(credential), Some("resource".into())) + } + _ => {} + } + } + }); + } + + #[test] + fn remove_credential() { + // Doesn't matter the flow here as this is for testing + // that the credentials are entered/retrieved correctly. + let mut oauth = OAuthSerializer::new(); + oauth + .client_id("bb301aaa-1201-4259-a230923fds32") + .redirect_uri("http://localhost:8888/redirect") + .client_secret("CLDIE3F") + .authorization_url("https://www.example.com/authorize?") + .refresh_token_url("https://www.example.com/token?") + .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); + assert!(oauth.get(OAuthParameter::ClientId).is_some()); + oauth.remove(OAuthParameter::ClientId); + assert!(oauth.get(OAuthParameter::ClientId).is_none()); + oauth.client_id("client_id"); + assert!(oauth.get(OAuthParameter::ClientId).is_some()); + + assert!(oauth.get(OAuthParameter::RedirectUri).is_some()); + oauth.remove(OAuthParameter::RedirectUri); + assert!(oauth.get(OAuthParameter::RedirectUri).is_none()); + } + + #[test] + fn setters() { + // Doesn't matter the flow here as this is for testing + // that the credentials are entered/retrieved correctly. + let mut oauth = OAuthSerializer::new(); + oauth + .client_id("client_id") + .client_secret("client_secret") + .authorization_url("https://example.com/authorize") + .refresh_token_url("https://example.com/token") + .token_uri("https://example.com/token") + .redirect_uri("https://example.com/redirect") + .authorization_code("access_code"); + + let test_setter = |c: OAuthParameter, s: &str| { + let result = oauth.get(c); + assert!(result.is_some()); + assert!(result.is_some()); + assert_eq!(result.unwrap(), s); + }; + + test_setter(OAuthParameter::ClientId, "client_id"); + test_setter(OAuthParameter::ClientSecret, "client_secret"); + test_setter( + OAuthParameter::AuthorizationUrl, + "https://example.com/authorize", + ); + test_setter(OAuthParameter::RefreshTokenUrl, "https://example.com/token"); + test_setter(OAuthParameter::TokenUrl, "https://example.com/token"); + test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); + test_setter(OAuthParameter::AuthorizationCode, "access_code"); + } +} diff --git a/graph-oauth/src/grants.rs b/graph-oauth/src/grants.rs deleted file mode 100644 index d86fa3ac..00000000 --- a/graph-oauth/src/grants.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::auth::OAuthParameter; - -#[derive( - Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, -)] -pub enum GrantRequest { - Authorization, - AccessToken, - RefreshToken, -} - -#[derive( - Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, -)] -pub enum GrantType { - TokenFlow, - CodeFlow, - AuthorizationCode, - Implicit, - DeviceCode, - OpenId, - ClientCredentials, - ResourceOwnerPasswordCredentials, -} - -impl GrantType { - pub fn available_credentials(self, grant_request: GrantRequest) -> Vec<OAuthParameter> { - match self { - GrantType::TokenFlow => match grant_request { - GrantRequest::Authorization - | GrantRequest::AccessToken - | GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::ResponseType, - OAuthParameter::Scope, - ], - }, - GrantType::CodeFlow => match grant_request { - GrantRequest::Authorization => vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::State, - OAuthParameter::ResponseType, - OAuthParameter::Scope, - ], - GrantRequest::AccessToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::ResponseType, - OAuthParameter::GrantType, - OAuthParameter::AuthorizationCode, - ], - GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::GrantType, - OAuthParameter::AuthorizationCode, - OAuthParameter::RefreshToken, - ], - }, - GrantType::AuthorizationCode => match grant_request { - GrantRequest::Authorization => vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::State, - OAuthParameter::ResponseMode, - OAuthParameter::ResponseType, - OAuthParameter::Scope, - OAuthParameter::Prompt, - OAuthParameter::DomainHint, - OAuthParameter::LoginHint, - OAuthParameter::CodeChallenge, - OAuthParameter::CodeChallengeMethod, - ], - GrantRequest::AccessToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::AuthorizationCode, - OAuthParameter::Scope, - OAuthParameter::GrantType, - OAuthParameter::CodeVerifier, - ], - GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RefreshToken, - OAuthParameter::GrantType, - OAuthParameter::Scope, - ], - }, - GrantType::Implicit => match grant_request { - GrantRequest::Authorization - | GrantRequest::AccessToken - | GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::Scope, - OAuthParameter::ResponseType, - OAuthParameter::ResponseMode, - OAuthParameter::State, - OAuthParameter::Nonce, - OAuthParameter::Prompt, - OAuthParameter::LoginHint, - OAuthParameter::DomainHint, - ], - }, - GrantType::OpenId => match grant_request { - GrantRequest::Authorization => vec![ - OAuthParameter::ClientId, - OAuthParameter::ResponseType, - OAuthParameter::RedirectUri, - OAuthParameter::ResponseMode, - OAuthParameter::Scope, - OAuthParameter::State, - OAuthParameter::Nonce, - OAuthParameter::Prompt, - OAuthParameter::LoginHint, - OAuthParameter::DomainHint, - OAuthParameter::Resource, - ], - GrantRequest::AccessToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::GrantType, - OAuthParameter::Scope, - OAuthParameter::AuthorizationCode, - OAuthParameter::CodeVerifier, - ], - GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RefreshToken, - OAuthParameter::GrantType, - OAuthParameter::Scope, - ], - }, - GrantType::ClientCredentials => match grant_request { - GrantRequest::Authorization => vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::State, - ], - GrantRequest::AccessToken | GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::GrantType, - OAuthParameter::Scope, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, - ], - }, - GrantType::ResourceOwnerPasswordCredentials => match grant_request { - GrantRequest::Authorization - | GrantRequest::AccessToken - | GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::GrantType, - OAuthParameter::Username, - OAuthParameter::Password, - OAuthParameter::Scope, - OAuthParameter::RedirectUri, - OAuthParameter::ClientAssertion, - ], - }, - GrantType::DeviceCode => match grant_request { - GrantRequest::Authorization => { - vec![OAuthParameter::ClientId, OAuthParameter::Scope] - } - GrantRequest::AccessToken => vec![ - OAuthParameter::GrantType, - OAuthParameter::ClientId, - OAuthParameter::DeviceCode, - ], - GrantRequest::RefreshToken => vec![ - OAuthParameter::ClientId, - OAuthParameter::Scope, - OAuthParameter::GrantType, - OAuthParameter::RefreshToken, - ], - }, - } - } -} diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index dfda56ae..6013bf7b 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -53,6 +53,7 @@ pub enum AuthorizationQueryError { pub struct AuthorizationQueryResponse { pub code: Option<String>, pub id_token: Option<String>, + pub expires_in: Option<String>, pub access_token: Option<String>, pub state: Option<String>, pub nonce: Option<String>, @@ -65,6 +66,17 @@ pub struct AuthorizationQueryResponse { log_pii: bool, } +impl AuthorizationQueryResponse { + /// Enable or disable logging of personally identifiable information such + /// as logging the id_token. This is disabled by default. When log_pii is enabled + /// passing [AuthorizationQueryResponse] to logging or print functions will log both the bearer + /// access token value of amy and the id token value. + /// By default these do not get logged. + pub fn enable_pii_logging(&mut self, log_pii: bool) { + self.log_pii = log_pii; + } +} + impl Debug for AuthorizationQueryResponse { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.log_pii { @@ -77,7 +89,7 @@ impl Debug for AuthorizationQueryResponse { .field("error", &self.error) .field("error_description", &self.error_description) .field("error_uri", &self.error_uri) - .field("additional_fields(serde flatten)", &self.additional_fields) + .field("additional_fields", &self.additional_fields) .finish() } else { f.debug_struct("AuthQueryResponse") @@ -89,7 +101,7 @@ impl Debug for AuthorizationQueryResponse { .field("error", &self.error) .field("error_description", &self.error_description) .field("error_uri", &self.error_uri) - .field("additional_fields(serde flatten)", &self.additional_fields) + .field("additional_fields", &self.additional_fields) .finish() } } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 138cb45b..9f6d65d4 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,5 +1,6 @@ use base64::Engine; use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; use reqwest::header::HeaderMap; use url::Url; @@ -8,7 +9,7 @@ use uuid::Uuid; use crate::identity::{Authority, AzureCloudInstance}; use crate::oauth::ForceTokenRefresh; -#[derive(Clone, Debug, Default, PartialEq)] +#[derive(Clone, Default, PartialEq)] pub struct AppConfig { /// The directory tenant that you want to request permission from. /// This can be in GUID or friendly name format. @@ -35,6 +36,32 @@ pub struct AppConfig { /// Cache id used in a token cache store. pub(crate) cache_id: String, pub(crate) force_token_refresh: ForceTokenRefresh, + pub(crate) log_pii: bool, +} + +impl Debug for AppConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.log_pii { + f.debug_struct("AppConfig") + .field("tenant_id", &self.tenant_id) + .field("client_id", &self.client_id) + .field("authority", &self.authority) + .field("azure_cloud_instance", &self.azure_cloud_instance) + .field("extra_query_parameters", &self.extra_query_parameters) + .field("extra_header_parameters", &self.extra_header_parameters) + .field("force_token_refresh", &self.force_token_refresh) + .finish() + } else { + f.debug_struct("AppConfig") + .field("tenant_id", &self.tenant_id) + .field("client_id", &self.client_id) + .field("authority", &self.authority) + .field("azure_cloud_instance", &self.azure_cloud_instance) + .field("extra_query_parameters", &self.extra_query_parameters) + .field("force_token_refresh", &self.force_token_refresh) + .finish() + } + } } impl AppConfig { @@ -53,6 +80,7 @@ impl AppConfig { redirect_uri: None, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), } } @@ -84,6 +112,7 @@ impl AppConfig { redirect_uri, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), } } @@ -102,6 +131,7 @@ impl AppConfig { redirect_uri: None, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), } } @@ -127,6 +157,11 @@ impl AppConfig { redirect_uri: None, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), } } + + pub fn log_pii(&mut self, log_pii: bool) { + self.log_pii = log_pii; + } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 5b71a16a..882ee862 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -4,7 +4,7 @@ use crate::identity::{ AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, ClientAssertionCredentialBuilder, ClientCredentialsAuthorizationUrlParameterBuilder, ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, - EnvironmentCredential, OpenIdAuthorizationUrlBuilder, OpenIdCredentialBuilder, + EnvironmentCredential, OpenIdAuthorizationUrlParameterBuilder, OpenIdCredentialBuilder, PublicClientApplication, ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, }; @@ -16,7 +16,10 @@ use std::env::VarError; use uuid::Uuid; #[cfg(feature = "openssl")] -use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; +use crate::identity::{ + AuthorizationCodeCertificateCredentialBuilder, ClientCertificateCredentialBuilder, + X509Certificate, +}; pub struct ConfidentialClientApplicationBuilder { pub(crate) app_config: AppConfig, @@ -85,13 +88,13 @@ impl ConfidentialClientApplicationBuilder { self } - pub fn auth_code_authorization_url_builder( - &mut self, - ) -> AuthCodeAuthorizationUrlParameterBuilder { + /// Auth Code Authorization Url Builder + pub fn auth_code_url_builder(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) } - pub fn client_credentials_authorization_url_builder( + /// Client Credentials Authorization Url Builder + pub fn client_credential_url_builder( &mut self, ) -> ClientCredentialsAuthorizationUrlParameterBuilder { ClientCredentialsAuthorizationUrlParameterBuilder::new_with_app_config( @@ -99,10 +102,12 @@ impl ConfidentialClientApplicationBuilder { ) } - pub fn openid_authorization_url_builder(&mut self) -> OpenIdAuthorizationUrlBuilder { - OpenIdAuthorizationUrlBuilder::new_with_app_config(self.app_config.clone()) + /// OpenId Authorization Url Builder + pub fn openid_url_builder(&mut self) -> OpenIdAuthorizationUrlParameterBuilder { + OpenIdAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) } + /// Client Credentials Using X509 Certificate #[cfg(feature = "openssl")] pub fn with_client_x509_certificate( self, @@ -111,6 +116,7 @@ impl ConfidentialClientApplicationBuilder { ClientCertificateCredentialBuilder::new_with_certificate(certificate, self.app_config) } + /// Client Credentials Using Client Secret. pub fn with_client_secret( self, client_secret: impl AsRef<str>, @@ -118,6 +124,7 @@ impl ConfidentialClientApplicationBuilder { ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) } + /// Client Credentials Using Assertion. pub fn with_client_assertion( self, signed_assertion: impl AsRef<str>, @@ -128,14 +135,16 @@ impl ConfidentialClientApplicationBuilder { ) } - pub fn with_authorization_code( + /// Client Credentials Authorization Url Builder + pub fn with_auth_code( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { AuthorizationCodeCredentialBuilder::new_with_auth_code(self.into(), authorization_code) } - pub fn with_authorization_code_assertion( + /// Auth Code Using Assertion + pub fn with_auth_code_assertion( self, authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, @@ -147,6 +156,7 @@ impl ConfidentialClientApplicationBuilder { ) } + /// Auth Code Using X509 Certificate #[cfg(feature = "openssl")] pub fn with_authorization_code_x509_certificate( self, @@ -160,6 +170,9 @@ impl ConfidentialClientApplicationBuilder { ) } + //#[cfg(feature = "interactive-auth")] + + /// Auth Code Using OpenId. pub fn with_openid( self, authorization_code: impl AsRef<str>, @@ -225,6 +238,7 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { redirect_uri: None, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), }, }) } @@ -378,6 +392,7 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { redirect_uri: None, cache_id, force_token_refresh: Default::default(), + log_pii: Default::default(), }, }) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index bce4ced8..c8432c93 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,28 +1,33 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; +use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use url::form_urlencoded::Serializer; use url::Url; use uuid::Uuid; +use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; use graph_error::{IdentityResult, AF}; -use graph_extensions::crypto::{secure_random_32, ProofKeyCodeExchange}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, + Authority, AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, + ConfidentialClientApplication, Prompt, ResponseMode, ResponseType, }; -#[cfg(feature = "interactive-auth")] +//#[cfg(feature = "interactive-auth")] use crate::identity::AuthorizationQueryResponse; +use crate::oauth::Token; -#[cfg(feature = "interactive-auth")] +//#[cfg(feature = "interactive-auth")] use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, }; +credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); + /// Get the authorization url required to perform the initial authorization and redirect in the /// authorization code flow. /// @@ -38,7 +43,7 @@ use crate::web::{ /// Reference: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code /// /// # Build a confidential client for the authorization code grant. -/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_authorization_code) to set the authorization code received from +/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_auth_code) to set the authorization code received from /// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) /// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder) /// to build the url that the user will be directed to authorize at. @@ -47,7 +52,7 @@ use crate::web::{ /// # use graph_oauth::identity::ConfidentialClientApplication;/// /// /// let client_app = ConfidentialClientApplication::builder("client-id") -/// .with_authorization_code("access-code") +/// .auth_code_url_builder() /// .with_client_secret("client-secret") /// .with_scope(vec!["User.Read"]) /// .build(); @@ -181,7 +186,7 @@ impl AuthCodeAuthorizationUrlParameters { self.nonce.as_ref() } - #[cfg(feature = "interactive-auth")] + //#[cfg(feature = "interactive-auth")] pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, @@ -194,7 +199,7 @@ impl AuthCodeAuthorizationUrlParameters { } return match next { - None => unreachable!(), + None => Err(anyhow::anyhow!("Unknown")), Some(auth_event) => { match auth_event { InteractiveAuthEvent::InvalidRedirectUri(reason) => { @@ -229,8 +234,8 @@ impl AuthCodeAuthorizationUrlParameters { } } -#[cfg(feature = "interactive-auth")] -mod web_view_authenticator { +// #[cfg(feature = "interactive-auth")] +pub(crate) mod web_view_authenticator { use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, @@ -398,7 +403,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { #[derive(Clone)] pub struct AuthCodeAuthorizationUrlParameterBuilder { - parameters: AuthCodeAuthorizationUrlParameters, + credential: AuthCodeAuthorizationUrlParameters, } impl AuthCodeAuthorizationUrlParameterBuilder { @@ -406,7 +411,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { - parameters: AuthCodeAuthorizationUrlParameters { + credential: AuthCodeAuthorizationUrlParameters { app_config: AppConfig::new_with_client_id(client_id.as_ref()), response_mode: None, response_type, @@ -428,7 +433,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { - parameters: AuthCodeAuthorizationUrlParameters { + credential: AuthCodeAuthorizationUrlParameters { app_config, response_mode: None, response_type, @@ -445,10 +450,11 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> &mut Self { - self.parameters.app_config.redirect_uri = Some(redirect_uri.into_url().unwrap()); + self.credential.app_config.redirect_uri = Some(redirect_uri.into_url().unwrap()); self } + /* pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { self.parameters.app_config.client_id = Uuid::try_parse(client_id.as_ref()).expect("Invalid Client Id - Must be a Uuid "); @@ -457,7 +463,9 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); + let tenant_id = tenant.as_ref(); + self.parameters.app_config.tenant_id = Some(tenant_id.to_owned()); + self.parameters.app_config.authority = Authority::TenantId(tenant_id.to_owned()); self } @@ -465,6 +473,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self.parameters.app_config.authority = authority.into(); self } + */ /// Default is code. Must include code for the authorization code flow. /// Can also include id_token or token if using the hybrid flow. @@ -472,7 +481,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { &mut self, response_type: I, ) -> &mut Self { - self.parameters + self.credential .response_type .extend(response_type.into_iter()); self @@ -490,7 +499,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.parameters.response_mode = Some(response_mode); + self.credential.response_mode = Some(response_mode); self } @@ -502,33 +511,19 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// Setting the nonce will override the nonce that is automatically generated by the /// credential client. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - self.parameters.nonce = Some(nonce.as_ref().to_owned()); + self.credential.nonce = Some(nonce.as_ref().to_owned()); self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.parameters.state = Some(state.as_ref().to_owned()); - self - } - - /// Required. - /// A space-separated list of scopes that you want the user to consent to. - /// For the /authorize leg of the request, this parameter can cover multiple resources. - /// This value allows your app to get consent for multiple web APIs you want to call. - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.parameters.scope.extend( - scope - .into_iter() - .map(|s| s.to_string()) - .map(|s| s.trim().to_owned()), - ); + self.credential.state = Some(state.as_ref().to_owned()); self } /// Adds the `offline_access` scope parameter which tells the authorization server /// to include a refresh token in the redirect uri query. pub fn with_offline_access(&mut self) -> &mut Self { - self.parameters + self.credential .scope .extend(vec!["offline_access".to_owned()]); self @@ -545,24 +540,24 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.parameters.prompt = Some(prompt); + self.credential.prompt = Some(prompt); self } pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.credential.domain_hint = Some(domain_hint.as_ref().to_owned()); self } pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.parameters.login_hint = Some(login_hint.as_ref().to_owned()); + self.credential.login_hint = Some(login_hint.as_ref().to_owned()); self } /// Used to secure authorization code grants by using Proof Key for Code Exchange (PKCE). /// Required if code_challenge_method is included. pub fn with_code_challenge<T: AsRef<str>>(&mut self, code_challenge: T) -> &mut Self { - self.parameters.code_challenge = Some(code_challenge.as_ref().to_owned()); + self.credential.code_challenge = Some(code_challenge.as_ref().to_owned()); self } @@ -575,7 +570,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { &mut self, code_challenge_method: T, ) -> &mut Self { - self.parameters.code_challenge_method = Some(code_challenge_method.as_ref().to_owned()); + self.credential.code_challenge_method = Some(code_challenge_method.as_ref().to_owned()); self } @@ -588,12 +583,36 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } + pub fn interactive_authentication( + &self, + options: Option<WebViewOptions>, + ) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { + let query_response = self + .credential + .interactive_webview_authentication(options)?; + if let Some(authorization_code) = query_response.code.as_ref() { + Ok(AuthorizationCodeCredentialBuilder::new_with_auth_code( + self.credential.app_config.clone(), + authorization_code, + )) + } else { + Ok(AuthorizationCodeCredentialBuilder::new_with_token( + self.credential.app_config.clone(), + Token::from(query_response), + )) + } + } + pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { - self.parameters.clone() + self.credential.clone() + } + + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { + self.credential.url_with_host(azure_cloud_instance) } pub fn url(&self) -> IdentityResult<Url> { - self.parameters.url() + self.credential.url() } } @@ -614,12 +633,11 @@ mod test { #[test] fn url_with_host() { - let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url_result = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) - .build(); + .url_with_host(&AzureCloudInstance::AzureGermany); - let url_result = authorizer.url_with_host(&AzureCloudInstance::AzureGermany); assert!(url_result.is_ok()); } diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 3c86d708..e67e0761 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -7,13 +7,14 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_error::{IdentityResult, AF}; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_error::{AuthExecutionError, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, - ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, + ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; @@ -54,6 +55,7 @@ pub struct AuthorizationCodeAssertionCredential { /// consent to various resources, if an access token is requested. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, + token_cache: InMemoryCacheStore<Token>, } impl Debug for AuthorizationCodeAssertionCredential { @@ -92,6 +94,7 @@ impl AuthorizationCodeAssertionCredential { client_assertion: client_assertion.as_ref().to_owned(), scope: vec![], serializer: OAuthSerializer::new(), + token_cache: Default::default(), }) } @@ -111,6 +114,49 @@ impl AuthorizationCodeAssertionCredential { } } +#[async_trait] +impl TokenCache for AuthorizationCodeAssertionCredential { + type Token = Token; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let msal_token: Token = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let msal_token: Token = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let msal_token: Token = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: Token = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { fn uri(&mut self) -> IdentityResult<Url> { @@ -247,6 +293,7 @@ impl AuthorizationCodeAssertionCredentialBuilder { client_assertion: String::new(), scope: vec![], serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, } } @@ -266,6 +313,7 @@ impl AuthorizationCodeAssertionCredentialBuilder { client_assertion: assertion.as_ref().to_owned(), scope: vec![], serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index b07d6f3c..a1b7cdf1 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -7,14 +7,15 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_error::{IdentityResult, AF}; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_error::{AuthExecutionError, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, + AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, + TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; @@ -56,6 +57,7 @@ pub struct AuthorizationCodeCertificateCredential { /// consent to various resources, if an access token is requested. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, + token_cache: InMemoryCacheStore<Token>, } impl Debug for AuthorizationCodeCertificateCredential { @@ -94,6 +96,7 @@ impl AuthorizationCodeCertificateCredential { client_assertion: client_assertion.as_ref().to_owned(), scope: vec![], serializer: OAuthSerializer::new(), + token_cache: Default::default(), }) } @@ -117,6 +120,48 @@ impl AuthorizationCodeCertificateCredential { } } +#[async_trait] +impl TokenCache for AuthorizationCodeCertificateCredential { + type Token = Token; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let msal_token: Token = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let msal_token: Token = response.json()?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let msal_token: Token = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: Token = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn uri(&mut self) -> IdentityResult<Url> { @@ -239,21 +284,6 @@ pub struct AuthorizationCodeCertificateCredentialBuilder { } impl AuthorizationCodeCertificateCredentialBuilder { - fn new() -> AuthorizationCodeCertificateCredentialBuilder { - Self { - credential: AuthorizationCodeCertificateCredential { - app_config: Default::default(), - authorization_code: None, - refresh_token: None, - code_verifier: None, - client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: String::new(), - scope: vec![], - serializer: OAuthSerializer::new(), - }, - } - } - #[cfg(feature = "openssl")] pub(crate) fn new_with_auth_code_and_x509( app_config: AppConfig, @@ -270,6 +300,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { client_assertion: String::new(), scope: vec![], serializer: OAuthSerializer::new(), + token_cache: Default::default(), }, }; @@ -331,16 +362,6 @@ impl AuthorizationCodeCertificateCredentialBuilder { } } -impl From<AuthCodeAuthorizationUrlParameters> for AuthorizationCodeCertificateCredentialBuilder { - fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { - let mut builder = AuthorizationCodeCertificateCredentialBuilder::new(); - builder.credential.app_config = value.app_config; - builder.with_scope(value.scope); - - builder - } -} - impl From<AuthorizationCodeCertificateCredential> for AuthorizationCodeCertificateCredentialBuilder { diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index be62afd0..feae6f14 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -7,9 +7,9 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::crypto::ProofKeyCodeExchange; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; -use graph_extensions::crypto::ProofKeyCodeExchange; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -62,7 +62,7 @@ pub struct AuthorizationCodeCredential { /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryTokenStore<Token>, + token_cache: InMemoryCacheStore<Token>, } impl Debug for AuthorizationCodeCredential { @@ -75,6 +75,26 @@ impl Debug for AuthorizationCodeCredential { } impl AuthorizationCodeCredential { + pub(crate) fn from_token_and_app_config( + app_config: AppConfig, + token: Token, + ) -> AuthorizationCodeCredential { + let cache_id = app_config.cache_id.clone(); + let mut token_cache = InMemoryCacheStore::new(); + token_cache.store(cache_id, token); + + AuthorizationCodeCredential { + app_config, + authorization_code: None, + refresh_token: None, + client_secret: "".to_string(), + scope: vec![], + code_verifier: None, + serializer: Default::default(), + token_cache, + } + } + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; let new_token: Token = response.json()?; @@ -104,7 +124,7 @@ impl AuthorizationCodeCredential { } #[async_trait] -impl TokenCacheStore for AuthorizationCodeCredential { +impl TokenCache for AuthorizationCodeCredential { type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { @@ -294,6 +314,15 @@ impl AuthorizationCodeCredentialBuilder { } } + pub(crate) fn new_with_token( + app_config: AppConfig, + token: Token, + ) -> AuthorizationCodeCredentialBuilder { + Self { + credential: AuthorizationCodeCredential::from_token_and_app_config(app_config, token), + } + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self.credential.refresh_token = None; @@ -534,6 +563,7 @@ mod test { #[test] fn should_force_refresh_test() { + let uuid_value = Uuid::new_v4().to_string(); let mut credential_builder = AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); let mut credential = credential_builder diff --git a/graph-oauth/src/identity/credentials/bearer_token_credential.rs b/graph-oauth/src/identity/credentials/bearer_token_credential.rs index 1195c7fd..bcc6e7cf 100644 --- a/graph-oauth/src/identity/credentials/bearer_token_credential.rs +++ b/graph-oauth/src/identity/credentials/bearer_token_credential.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; +use graph_core::cache::AsBearer; use graph_core::identity::ClientApplication; use graph_error::AuthExecutionResult; -use graph_extensions::cache::AsBearer; +use std::borrow::Cow; #[derive(Clone)] pub struct BearerTokenCredential(String); @@ -18,7 +19,7 @@ impl BearerTokenCredential { impl ToString for BearerTokenCredential { fn to_string(&self) -> String { - self.0.clone() + self.0.to_string() } } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index e81ddcb5..b1fd4154 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -6,15 +6,15 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; +use crate::auth::{OAuthParameter, OAuthSerializer}; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ForceTokenRefresh, Token, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, + Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, + TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; -use crate::oauth::{ConfidentialClientApplication, OAuthParameter, OAuthSerializer}; credential_builder!( ClientAssertionCredentialBuilder, @@ -33,7 +33,7 @@ pub struct ClientAssertionCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryTokenStore<Token>, + token_cache: InMemoryCacheStore<Token>, } impl ClientAssertionCredential { @@ -64,7 +64,7 @@ impl Debug for ClientAssertionCredential { } #[async_trait] -impl TokenCacheStore for ClientAssertionCredential { +impl TokenCache for ClientAssertionCredential { type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 26f3bf81..7afff746 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -39,6 +39,11 @@ macro_rules! credential_builder_base { self } + pub fn with_default_scope(&mut self) -> &mut Self { + self.credential.scope = vec!["https://graph.microsoft.com/.default".to_string()]; + self + } + /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { @@ -88,14 +93,6 @@ macro_rules! credential_builder_base { .extend(header_parameters); self } - - pub fn force_token_refresh( - &mut self, - force_token_refresh: ForceTokenRefresh, - ) -> &mut Self { - self.credential.app_config.force_token_refresh = force_token_refresh; - self - } } }; } @@ -108,6 +105,14 @@ macro_rules! credential_builder { pub fn build(&self) -> $client { <$client>::new(self.credential.clone()) } + + pub fn force_token_refresh( + &mut self, + force_token_refresh: ForceTokenRefresh, + ) -> &mut Self { + self.credential.app_config.force_token_refresh = force_token_refresh; + self + } } }; } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 2f64e0dd..4e4d1225 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -6,8 +6,8 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult, AF}; -use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -40,7 +40,7 @@ pub struct ClientCertificateCredential { pub(crate) client_assertion: String, pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, - token_cache: InMemoryTokenStore<Token>, + token_cache: InMemoryCacheStore<Token>, } impl ClientCertificateCredential { @@ -92,7 +92,7 @@ impl Debug for ClientCertificateCredential { } #[async_trait] -impl TokenCacheStore for ClientCertificateCredential { +impl TokenCache for ClientCertificateCredential { type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 00f450a7..2494a48a 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -6,8 +6,8 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; -use graph_extensions::cache::{InMemoryTokenStore, TokenCacheStore}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ @@ -50,7 +50,7 @@ pub struct ClientSecretCredential { /// Default is https://graph.microsoft.com/.default. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, - token_cache: InMemoryTokenStore<Token>, + token_cache: InMemoryCacheStore<Token>, } impl Debug for ClientSecretCredential { @@ -69,7 +69,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), - token_cache: InMemoryTokenStore::new(), + token_cache: InMemoryCacheStore::new(), } } @@ -83,7 +83,7 @@ impl ClientSecretCredential { client_secret: client_secret.as_ref().to_owned(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), - token_cache: InMemoryTokenStore::new(), + token_cache: InMemoryCacheStore::new(), } } @@ -95,7 +95,7 @@ impl ClientSecretCredential { } #[async_trait] -impl TokenCacheStore for ClientSecretCredential { +impl TokenCache for ClientSecretCredential { type Token = Token; fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { @@ -232,7 +232,7 @@ impl ClientSecretCredentialBuilder { client_secret: client_secret.as_ref().to_string(), scope: vec!["https://graph.microsoft.com/.default".into()], serializer: Default::default(), - token_cache: InMemoryTokenStore::new(), + token_cache: InMemoryCacheStore::new(), }, } } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index ecc630eb..1801f01e 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -7,17 +7,21 @@ use reqwest::Response; use url::Url; use uuid::Uuid; +use graph_core::cache::{AsBearer, TokenCache}; use graph_core::identity::ClientApplication; -use graph_error::{AuthExecutionResult, IdentityResult}; -use graph_extensions::cache::{AsBearer, TokenCacheStore}; +use graph_error::{AuthExecutionResult, GraphResult, IdentityResult, AF}; use crate::identity::{ credentials::app_config::AppConfig, credentials::application_builder::ConfidentialClientApplicationBuilder, credentials::client_assertion_credential::ClientAssertionCredential, Authority, AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, - AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, - ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, + AuthorizationCodeCredential, AuthorizationQueryResponse, AzureCloudInstance, + ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, +}; +use crate::oauth::{AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters}; +use crate::web::{ + InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, }; /// Clients capable of maintaining the confidentiality of their credentials @@ -26,7 +30,7 @@ use crate::identity::{ /// /// /// # Build a confidential client for the authorization code grant. -/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_authorization_code) to set the authorization code received from +/// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_auth_code) to set the authorization code received from /// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) /// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder) /// to build the url that the user will be directed to authorize at. @@ -43,7 +47,25 @@ impl ConfidentialClientApplication<()> { } } -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> +impl ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { + pub fn parameter_builder( + credential: AuthCodeAuthorizationUrlParameters, + ) -> ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { + ConfidentialClientApplication { credential } + } + + pub async fn interactive_auth( + &self, + options: Option<WebViewOptions>, + ) -> anyhow::Result<AuthorizationQueryResponse> { + let result = self + .credential + .interactive_webview_authentication(options)?; + Ok(result) + } +} + +impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> ConfidentialClientApplication<Credential> { pub(crate) fn new(credential: Credential) -> ConfidentialClientApplication<Credential> { @@ -60,7 +82,7 @@ impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> } #[async_trait] -impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication +impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication for ConfidentialClientApplication<Credential> { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { @@ -75,7 +97,7 @@ impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication } #[async_trait] -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor +impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> TokenCredentialExecutor for ConfidentialClientApplication<Credential> { fn uri(&mut self) -> IdentityResult<Url> { @@ -165,6 +187,64 @@ impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> } } +impl From<AuthCodeAuthorizationUrlParameters> + for ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> +{ + fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { + ConfidentialClientApplication::parameter_builder(value) + } +} + +impl ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { + pub fn interactive_webview_authentication( + &self, + interactive_web_view_options: Option<WebViewOptions>, + ) -> anyhow::Result<AuthorizationQueryResponse> { + let receiver = self + .credential + .interactive_authentication(interactive_web_view_options)?; + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + while next.is_none() { + next = iter.next(); + } + + return match next { + None => Err(anyhow::anyhow!("Unknown")), + Some(auth_event) => { + match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::TimedOut(duration) => { + Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => { + Err(anyhow::anyhow!("CloseRequested")) + } + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) + } + } + } + } + } + }; + } +} + #[cfg(test)] mod test { use crate::identity::Authority; @@ -177,7 +257,7 @@ mod test { let client_id_string = client_id.to_string(); let mut confidential_client = ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") + .with_auth_code("code") .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_scope(vec!["Read.Write"]) .with_redirect_uri("http://localhost:8888/redirect") @@ -198,7 +278,7 @@ mod test { let client_id_string = client_id.to_string(); let mut confidential_client = ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") + .with_auth_code("code") .with_tenant("tenant") .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_scope(vec!["Read.Write"]) @@ -220,7 +300,7 @@ mod test { let client_id_string = client_id.to_string(); let mut confidential_client = ConfidentialClientApplication::builder(client_id_string.as_str()) - .with_authorization_code("code") + .with_auth_code("code") .with_authority(Authority::Consumers) .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_scope(vec!["Read.Write", "Fall.Down"]) diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 251c3611..ee06a952 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -8,13 +8,13 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; +use graph_core::http::{ + AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, +}; use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, IdentityResult, AF, }; -use graph_extensions::http::{ - AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, -}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 65895f20..b25e4926 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,5 +1,5 @@ +use graph_core::crypto::secure_random_32; use graph_error::{AuthorizationFailure, IdentityResult}; -use graph_extensions::crypto::secure_random_32; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -335,7 +335,7 @@ impl ImplicitCredentialBuilder { self.credential.url() } - pub fn build_credential(&self) -> ImplicitCredential { + pub fn build(&self) -> ImplicitCredential { self.credential.clone() } } @@ -358,7 +358,7 @@ mod test { .with_nonce("678910") .with_prompt(Prompt::None) .with_login_hint("myuser@mycompany.com") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -375,7 +375,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -394,7 +394,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -413,7 +413,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -436,7 +436,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -455,7 +455,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -474,7 +474,7 @@ mod test { .unwrap() .with_scope(["User.Read"]) .with_nonce("678910") - .build_credential(); + .build(); let url_result = authorizer.url(); assert!(url_result.is_ok()); @@ -493,7 +493,7 @@ mod test { .with_redirect_uri("https://example.com/myapp") .unwrap() .with_nonce("678910") - .build_credential(); + .build(); let _ = authorizer.url().unwrap(); } diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index cc6f7e90..842a6cf2 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -6,8 +6,8 @@ use url::form_urlencoded::Serializer; use url::Url; use uuid::Uuid; +use graph_core::crypto::secure_random_32; use graph_error::{AuthorizationFailure, IdentityResult, AF}; -use graph_extensions::crypto::secure_random_32; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -20,7 +20,7 @@ use crate::identity::{ /// OAuth-enabled applications by using a security token called an ID token. /// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc #[derive(Clone)] -pub struct OpenIdAuthorizationUrl { +pub struct OpenIdAuthorizationUrlParameters { pub(crate) app_config: AppConfig, /// Required - /// Must include code for OpenID Connect sign-in. @@ -106,7 +106,7 @@ pub struct OpenIdAuthorizationUrl { response_types_supported: Vec<String>, } -impl Debug for OpenIdAuthorizationUrl { +impl Debug for OpenIdAuthorizationUrlParameters { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("AuthCodeAuthorizationUrlParameters") .field("app_config", &self.app_config) @@ -117,12 +117,12 @@ impl Debug for OpenIdAuthorizationUrl { .finish() } } -impl OpenIdAuthorizationUrl { +impl OpenIdAuthorizationUrlParameters { pub fn new<T: AsRef<str>, IU: IntoUrl, U: ToString, I: IntoIterator<Item = U>>( client_id: T, redirect_uri: IU, scope: I, - ) -> IdentityResult<OpenIdAuthorizationUrl> { + ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { let mut tree_set_scope = BTreeSet::new(); tree_set_scope.insert("openid".to_owned()); tree_set_scope.extend(scope.into_iter().map(|s| s.to_string())); @@ -134,7 +134,7 @@ impl OpenIdAuthorizationUrl { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::IdToken); - Ok(OpenIdAuthorizationUrl { + Ok(OpenIdAuthorizationUrlParameters { app_config, response_type, response_mode: None, @@ -153,14 +153,16 @@ impl OpenIdAuthorizationUrl { }) } - fn new_with_app_config(app_config: AppConfig) -> IdentityResult<OpenIdAuthorizationUrl> { + fn new_with_app_config( + app_config: AppConfig, + ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { let mut scope = BTreeSet::new(); scope.insert("openid".to_owned()); let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::IdToken); - Ok(OpenIdAuthorizationUrl { + Ok(OpenIdAuthorizationUrlParameters { app_config, response_type, response_mode: None, @@ -179,8 +181,10 @@ impl OpenIdAuthorizationUrl { }) } - pub fn builder(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { - OpenIdAuthorizationUrlBuilder::new(client_id) + pub fn builder( + client_id: impl AsRef<str>, + ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { + OpenIdAuthorizationUrlParameterBuilder::new(client_id) } pub fn url(&self) -> IdentityResult<Url> { @@ -202,7 +206,7 @@ impl OpenIdAuthorizationUrl { } } -impl AuthorizationUrl for OpenIdAuthorizationUrl { +impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { fn redirect_uri(&self) -> Option<&Url> { self.app_config.redirect_uri.as_ref() } @@ -312,25 +316,29 @@ impl AuthorizationUrl for OpenIdAuthorizationUrl { } } -pub struct OpenIdAuthorizationUrlBuilder { - parameters: OpenIdAuthorizationUrl, +pub struct OpenIdAuthorizationUrlParameterBuilder { + parameters: OpenIdAuthorizationUrlParameters, } -impl OpenIdAuthorizationUrlBuilder { - pub(crate) fn new(client_id: impl AsRef<str>) -> IdentityResult<OpenIdAuthorizationUrlBuilder> { - Ok(OpenIdAuthorizationUrlBuilder { - parameters: OpenIdAuthorizationUrl::new_with_app_config( +impl OpenIdAuthorizationUrlParameterBuilder { + pub(crate) fn new( + client_id: impl AsRef<str>, + ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { + Ok(OpenIdAuthorizationUrlParameterBuilder { + parameters: OpenIdAuthorizationUrlParameters::new_with_app_config( AppConfig::new_with_client_id(client_id), )?, }) } - pub(crate) fn new_with_app_config(app_config: AppConfig) -> OpenIdAuthorizationUrlBuilder { + pub(crate) fn new_with_app_config( + app_config: AppConfig, + ) -> OpenIdAuthorizationUrlParameterBuilder { let mut scope = BTreeSet::new(); scope.insert("openid".to_owned()); - OpenIdAuthorizationUrlBuilder { - parameters: OpenIdAuthorizationUrl::new_with_app_config(app_config) + OpenIdAuthorizationUrlParameterBuilder { + parameters: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config) .expect("ring::crypto::Unspecified"), } } @@ -469,7 +477,7 @@ impl OpenIdAuthorizationUrlBuilder { self } - pub fn build(&self) -> OpenIdAuthorizationUrl { + pub fn build(&self) -> OpenIdAuthorizationUrlParameters { self.parameters.clone() } @@ -485,7 +493,7 @@ mod test { #[test] #[should_panic] fn unsupported_response_type() { - let _ = OpenIdAuthorizationUrl::builder("client_id") + let _ = OpenIdAuthorizationUrlParameters::builder("client_id") .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) .with_scope(["scope"]) diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index b5873762..2fada234 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -2,20 +2,22 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use async_trait::async_trait; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_error::{IdentityResult, AF}; -use graph_extensions::crypto::{GenPkce, ProofKeyCodeExchange}; +use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, - OpenIdAuthorizationUrl, OpenIdAuthorizationUrlBuilder, TokenCredentialExecutor, + OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, + TokenCredentialExecutor, }; +use crate::internal::{OAuthParameter, OAuthSerializer}; credential_builder!( OpenIdCredentialBuilder, @@ -64,6 +66,7 @@ pub struct OpenIdCredential { /// is called. pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: OAuthSerializer, + token_cache: InMemoryCacheStore<Token>, } impl Debug for OpenIdCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -94,7 +97,8 @@ impl OpenIdCredential { scope: vec!["openid".to_owned()], code_verifier: None, pkce: None, - serializer: OAuthSerializer::new(), + serializer: Default::default(), + token_cache: Default::default(), }) } @@ -107,13 +111,128 @@ impl OpenIdCredential { OpenIdCredentialBuilder::new() } - pub fn authorization_url_builder(client_id: impl AsRef<str>) -> OpenIdAuthorizationUrlBuilder { - OpenIdAuthorizationUrlBuilder::new_with_app_config(AppConfig::new_with_client_id(client_id)) + pub fn authorization_url_builder( + client_id: impl AsRef<str>, + ) -> OpenIdAuthorizationUrlParameterBuilder { + OpenIdAuthorizationUrlParameterBuilder::new_with_app_config(AppConfig::new_with_client_id( + client_id, + )) } pub fn pkce(&self) -> Option<&ProofKeyCodeExchange> { self.pkce.as_ref() } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } +} + +#[async_trait] +impl TokenCache for OpenIdCredential { + type Token = Token; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + match self.execute_cached_token_refresh(cache_id.clone()) { + Ok(token) => return Ok(token), + Err(_) => {} + } + } + + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh(cache_id) + } else { + Ok(token.clone()) + } + } else { + self.execute_cached_token_refresh(cache_id) + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh(cache_id); + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result + } + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + match self + .execute_cached_token_refresh_async(cache_id.clone()) + .await + { + Ok(token) => return Ok(token), + Err(_) => {} + } + } + + if let Some(old_token) = self.token_cache.get(cache_id.as_str()) { + if old_token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = old_token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh_async(cache_id).await + } else { + Ok(old_token.clone()) + } + } else { + self.execute_cached_token_refresh_async(cache_id).await + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh_async(cache_id).await; + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result + } + } + } } #[async_trait] @@ -248,7 +367,8 @@ impl OpenIdCredentialBuilder { scope: vec!["openid".to_owned()], code_verifier: None, pkce: None, - serializer: OAuthSerializer::new(), + serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -268,6 +388,7 @@ impl OpenIdCredentialBuilder { code_verifier: None, pkce: None, serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -318,8 +439,8 @@ impl OpenIdCredentialBuilder { } } -impl From<OpenIdAuthorizationUrl> for OpenIdCredentialBuilder { - fn from(value: OpenIdAuthorizationUrl) -> Self { +impl From<OpenIdAuthorizationUrlParameters> for OpenIdCredentialBuilder { + fn from(value: OpenIdAuthorizationUrlParameters) -> Self { let mut builder = OpenIdCredentialBuilder::new(); builder.credential.app_config = value.app_config; builder.with_scope(value.scope); diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index d1ce8141..214f6358 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -5,9 +5,9 @@ use crate::identity::{ TokenCredentialExecutor, }; use async_trait::async_trait; +use graph_core::cache::{AsBearer, TokenCache}; use graph_core::identity::ClientApplication; use graph_error::{AuthExecutionResult, IdentityResult}; -use graph_extensions::cache::{AsBearer, TokenCacheStore}; use reqwest::Response; use std::collections::HashMap; use std::fmt::Debug; @@ -30,7 +30,7 @@ impl PublicClientApplication<()> { } } -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> +impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> PublicClientApplication<Credential> { pub(crate) fn new(credential: Credential) -> PublicClientApplication<Credential> { @@ -43,7 +43,7 @@ impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> } #[async_trait] -impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication +impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication for PublicClientApplication<Credential> { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { @@ -58,7 +58,7 @@ impl<Credential: Clone + Debug + Send + TokenCacheStore> ClientApplication } #[async_trait] -impl<Credential: Clone + Debug + Send + TokenCredentialExecutor> TokenCredentialExecutor +impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> TokenCredentialExecutor for PublicClientApplication<Credential> { fn uri(&mut self) -> IdentityResult<Url> { diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index dfc4a864..46f945b6 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -5,7 +5,6 @@ use async_trait::async_trait; use dyn_clone::DynClone; use reqwest::header::HeaderMap; use reqwest::tls::Version; -use tracing::debug; use url::Url; use uuid::Uuid; @@ -54,9 +53,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .headers(auth_request.headers) .form(&auth_request.form_urlencoded); - debug!( - "authorization request constructed; request={:#?}", - request_builder + tracing::debug!( + "authorization request constructed; request_builder={request_builder:#?}" ); Ok(request_builder) } else { @@ -65,9 +63,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .headers(auth_request.headers) .form(&auth_request.form_urlencoded); - debug!( - "authorization request constructed; request={:#?}", - request_builder + tracing::debug!( + "authorization request constructed; request_builder={request_builder:#?}" ); Ok(request_builder) } @@ -90,9 +87,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .headers(auth_request.headers) .form(&auth_request.form_urlencoded); - debug!( - "authorization request constructed; request={:#?}", - request_builder + tracing::debug!( + "authorization request constructed; request_builder={request_builder:#?}" ); Ok(request_builder) } else { @@ -101,9 +97,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .headers(auth_request.headers) .form(&auth_request.form_urlencoded); - debug!( - "authorization request constructed; request={:#?}", - request_builder + tracing::debug!( + "authorization request constructed; request_builder={request_builder:#?}" ); Ok(request_builder) } @@ -139,7 +134,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let request_builder = self.build()?; let response = request_builder.send()?; - debug!("authorization response received; response={:#?}", response); + tracing::debug!("authorization response received; response={response:#?}"); Ok(response) } @@ -147,92 +142,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { let request_builder = self.build_async()?; let response = request_builder.send().await?; - debug!("authorization response received; response={:#?}", response); + tracing::debug!("authorization response received; response={response:#?}"); Ok(response) } } - -/* - fn openid_configuration_url(&self) -> IdentityResult<Url> { - Ok(Url::parse( - format!( - "{}/{}/v2.0/.well-known/openid-configuration", - self.azure_cloud_instance().as_ref(), - self.authority().as_ref() - ) - .as_str(), - )?) - } - - fn get_openid_config(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let open_id_url = self.openid_configuration_url()?; - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - - let response = http_client - .get(open_id_url) - .headers(headers) - .send() - .expect("Error on header"); - - Ok(response) - } - - async fn get_openid_config_async(&mut self) -> AuthExecutionResult<reqwest::Response> { - let open_id_config_url = self.openid_configuration_url()?; - let http_client = ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - - let response = http_client - .get(open_id_config_url) - .headers(headers) - .send() - .await?; - - println!("{:#?}", response); - - Ok(response) - } - - #[cfg(test)] -mod test { - use super::*; - use crate::identity::credentials::application_builder::ConfidentialClientApplicationBuilder; - - #[test] - fn open_id_configuration_url_authority_tenant_id() { - let open_id = ConfidentialClientApplicationBuilder::new("client-id") - .with_openid("auth-code", "client-secret") - .with_tenant("tenant-id") - .build(); - - let url = open_id.openid_configuration_url().unwrap(); - assert_eq!( - "https://login.microsoftonline.com/tenant-id/v2.0/.well-known/openid-configuration", - url.as_str() - ) - } - - #[test] - fn open_id_configuration_url_authority_common() { - let open_id = ConfidentialClientApplicationBuilder::new("client-id") - .with_openid("auth-code", "client-secret") - .build(); - - let url = open_id.openid_configuration_url().unwrap(); - assert_eq!( - "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", - url.as_str() - ) - } -} - - */ diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 22f1833c..fae00ab9 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -13,7 +13,7 @@ pub use authorization_serializer::*; pub use credentials::*; pub use device_code::*; pub use id_token::*; -pub use msal_token::*; +pub use token::*; pub use token_validator::*; mod allowed_host_validator; @@ -26,5 +26,5 @@ mod credentials; mod device_code; mod id_token; -mod msal_token; +mod token; mod token_validator; diff --git a/graph-oauth/src/identity/msal_token.rs b/graph-oauth/src/identity/token.rs similarity index 88% rename from graph-oauth/src/identity/msal_token.rs rename to graph-oauth/src/identity/token.rs index 3a4f464b..105a9d20 100644 --- a/graph-oauth/src/identity/msal_token.rs +++ b/graph-oauth/src/identity/token.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; use std::fmt; use std::ops::{Add, Sub}; -use crate::identity::IdToken; -use graph_extensions::cache::AsBearer; +use crate::identity::{AuthorizationQueryResponse, IdToken}; +use graph_core::cache::AsBearer; use std::str::FromStr; use time::OffsetDateTime; @@ -26,7 +26,7 @@ where // Used to set timestamp based on expires in // which can only be done after deserialization. #[derive(Debug, Clone, Serialize, Deserialize)] -struct PhantomMsalToken { +struct PhantomToken { access_token: String, token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] @@ -55,8 +55,8 @@ struct PhantomMsalToken { /// Create a new AccessToken. /// # Example /// ``` -/// # use graph_extensions::token::MsalToken; -/// let token_response = MsalToken::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); +/// # use graph_oauth::oauth::Token; +/// let token_response = Token::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); /// ``` /// The [Token::jwt] method attempts to parse the access token as a JWT. /// Tokens returned for personal microsoft accounts that use legacy MSA @@ -131,9 +131,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_token_type("Bearer"); /// ``` pub fn with_token_type(&mut self, s: &str) -> &mut Self { @@ -145,9 +145,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_expires_in(3600); /// ``` pub fn with_expires_in(&mut self, expires_in: i64) -> &mut Self { @@ -162,9 +162,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_scope(vec!["User.Read"]); /// ``` pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { @@ -176,9 +176,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_access_token("ASODFIUJ34KJ;LADSK"); /// ``` pub fn with_access_token(&mut self, s: &str) -> &mut Self { @@ -190,9 +190,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_refresh_token("#ASOD323U5342"); /// ``` pub fn with_refresh_token(&mut self, s: &str) -> &mut Self { @@ -204,9 +204,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_user_id("user_id"); /// ``` pub fn with_user_id(&mut self, s: &str) -> &mut Self { @@ -218,9 +218,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::{MsalToken, IdToken}; + /// # use graph_oauth::oauth::{Token, IdToken}; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.set_id_token("id_token"); /// ``` pub fn set_id_token(&mut self, s: &str) -> &mut Self { @@ -232,7 +232,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::identity::{Token, IdToken}; + /// # use graph_oauth::oauth::{Token, IdToken}; /// /// let mut access_token = Token::default(); /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); @@ -249,10 +249,10 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; - /// # use graph_extensions::token::IdToken; + /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::oauth::IdToken; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.with_state("state"); /// ``` pub fn with_state(&mut self, s: &str) -> &mut Self { @@ -289,9 +289,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// access_token.expires_in = 86999; /// access_token.gen_timestamp(); /// println!("{:#?}", access_token.timestamp); @@ -308,9 +308,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.is_expired()); /// ``` pub fn is_expired(&self) -> bool { @@ -327,9 +327,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.is_expired_sub(time::Duration::minutes(5))); /// ``` pub fn is_expired_sub(&self, duration: time::Duration) -> bool { @@ -347,9 +347,9 @@ impl Token { /// /// # Example /// ``` - /// # use graph_extensions::token::MsalToken; + /// # use graph_oauth::oauth::Token; /// - /// let mut access_token = MsalToken::default(); + /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.elapsed()); /// ``` pub fn elapsed(&self) -> Option<time::Duration> { @@ -381,6 +381,28 @@ impl Default for Token { } } +impl From<AuthorizationQueryResponse> for Token { + fn from(value: AuthorizationQueryResponse) -> Self { + Token { + access_token: value.access_token.unwrap_or_default(), + token_type: "Bearer".to_string(), + expires_in: 3600, + ext_expires_in: None, + scope: vec![], + refresh_token: None, + user_id: None, + id_token: value.id_token, + state: None, + correlation_id: None, + client_info: None, + timestamp: None, + expires_on: None, + additional_fields: Default::default(), + log_pii: false, + } + } +} + impl ToString for Token { fn to_string(&self) -> String { self.access_token.to_string() @@ -483,7 +505,7 @@ impl<'de> Deserialize<'de> for Token { where D: Deserializer<'de>, { - let phantom_access_token: PhantomMsalToken = Deserialize::deserialize(deserializer)?; + let phantom_access_token: PhantomToken = Deserialize::deserialize(deserializer)?; let timestamp = OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 8ec21296..24c639e7 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -35,7 +35,7 @@ //! //! pub fn authorization_url(client_id: &str) -> IdentityResult<Url> { //! ConfidentialClientApplication::builder(client_id) -//! .auth_code_authorization_url_builder() +//! .auth_code_url_builder() //! .with_redirect_uri("http://localhost:8000/redirect") //! .with_scope(vec!["user.read"]) //! .url() @@ -43,7 +43,7 @@ //! //! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { //! Ok(ConfidentialClientApplication::builder(client_id) -//! .with_authorization_code(authorization_code) +//! .with_auth_code(authorization_code) //! .with_client_secret(client_secret) //! .with_scope(vec!["user.read"]) //! .with_redirect_uri("http://localhost:8000/redirect")? @@ -56,29 +56,32 @@ extern crate serde; #[macro_use] extern crate strum; -mod auth; +pub(crate) mod auth; mod discovery; -mod grants; pub mod jwt; mod oauth_error; -pub mod identity; +pub(crate) mod identity; -#[cfg(feature = "interactive-auth")] +//#[cfg(feature = "interactive-auth")] pub(crate) mod web; pub(crate) mod internal { + pub use crate::auth::*; pub use graph_core::http::*; } +pub mod extensions { + pub use crate::auth::*; +} + pub mod oauth { - pub use graph_extensions::{crypto::GenPkce, crypto::ProofKeyCodeExchange}; + pub use graph_core::{crypto::GenPkce, crypto::ProofKeyCodeExchange}; - pub use crate::auth::OAuthParameter; - pub use crate::auth::OAuthSerializer; - pub use crate::discovery::graph_discovery; - pub use crate::discovery::jwt_keys; pub use crate::identity::*; - pub use crate::oauth_error::OAuthError; - pub use crate::strum::IntoEnumIterator; + + //#[cfg(feature = "interactive-auth")] + pub mod web { + pub use crate::web::*; + } } diff --git a/graph-oauth/src/oauth_error.rs b/graph-oauth/src/oauth_error.rs index a35e9c7b..3a502760 100644 --- a/graph-oauth/src/oauth_error.rs +++ b/graph-oauth/src/oauth_error.rs @@ -3,10 +3,9 @@ use std::error::Error; use std::fmt; use std::io::ErrorKind; -use graph_error::{GraphFailure, GraphResult}; +use graph_error::GraphFailure; use crate::auth::OAuthParameter; -use crate::grants::{GrantRequest, GrantType}; /// Error implementation for OAuth #[derive(Debug)] @@ -27,28 +26,12 @@ impl OAuthError { pub fn invalid(msg: &str) -> GraphFailure { OAuthError::error_kind(ErrorKind::InvalidData, msg) } - - pub fn error_from<T>(c: OAuthParameter) -> Result<T, GraphFailure> { - Err(OAuthError::credential_error(c)) - } - pub fn credential_error(c: OAuthParameter) -> GraphFailure { GraphFailure::error_kind( ErrorKind::NotFound, format!("MISSING OR INVALID: {c:#?}").as_str(), ) } - - pub fn grant_error<T>( - grant: GrantType, - grant_request: GrantRequest, - msg: &str, - ) -> GraphResult<T> { - let error_str = format!( - "There was an error for the grant: {grant:#?} when executing a request for: {grant_request:#?}\nError: {msg:#?}", - ); - OAuthError::invalid_data(error_str.as_str()) - } } impl fmt::Display for OAuthError { diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index 2ab5d97d..87caedfd 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -9,13 +9,13 @@ pub trait InteractiveAuthenticator { ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum WindowCloseReason { CloseRequested, InvalidWindowNavigation, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum InteractiveAuthEvent { InvalidRedirectUri(String), TimedOut(std::time::Duration), diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index cf00e505..3345cb26 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,5 +1,7 @@ use anyhow::Context; -use std::time::Duration; +use std::sync::mpsc::SendError; +use std::time::{Duration, Instant}; +use tracing::instrument::WithSubscriber; use url::Url; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; @@ -107,6 +109,7 @@ impl InteractiveWebView { options: WebViewOptions, sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, ) -> anyhow::Result<()> { + tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() .with_any_thread(true) @@ -120,6 +123,7 @@ impl InteractiveWebView { .with_content_protection(true) .with_minimizable(true) .with_maximizable(true) + .with_focused(true) .with_resizable(true) .with_theme(options.theme) .build(&event_loop)?; @@ -136,8 +140,7 @@ impl InteractiveWebView { if is_redirect { sender2 .send(InteractiveAuthEvent::ReachedRedirectUri(url.clone())) - .context("mpsc error") - .unwrap(); + .unwrap_or_default(); // Wait time to avoid deadlock where window closes before // the channel has received the redirect uri. std::thread::sleep(Duration::from_secs(1)); @@ -151,7 +154,7 @@ impl InteractiveWebView { is_valid_host } else { - tracing::info!("Unable to navigate WebView - Option<Url> was None"); + tracing::trace!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); let _ = proxy.send_event(UserEvents::CloseWindow); false } @@ -162,28 +165,34 @@ impl InteractiveWebView { *control_flow = ControlFlow::Wait; match event { - Event::NewEvents(StartCause::Init) => tracing::info!("Webview runtime started"), + Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { - tracing::info!("Window closing without reaching redirect uri"); + sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::CloseRequested)).unwrap_or_default(); + tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::info!("Matched on redirect uri: {uri:#?}"); - tracing::info!("Closing window"); + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?}"); + tracing::trace!(target: "interactive_webview", "Closing window"); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { - tracing::error!("WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); + tracing::error!(target: "interactive_webview", "WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); if options.close_window_on_invalid_uri_navigation { - tracing::error!("Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); - sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap(); + tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); + sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap_or_default(); + + // Clear browsing data in the event of invalid navigation as we don't + // know if there is a security issue. + let _ = webview.clear_all_browsing_data(); + // Wait time to avoid deadlock where window closes before // the channel has received last event. std::thread::sleep(Duration::from_secs(1)); - let _ = webview.clear_all_browsing_data(); + *control_flow = ControlFlow::Exit; } } diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index 8e6d9b7e..eb9220b7 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -1,11 +1,14 @@ use std::time::Duration; +pub use wry::application::window::Theme; + #[derive(Clone, Debug)] pub struct WebViewOptions { + pub window_title: String, // Close window if navigation to a uri that does not match one of the // given redirect uri's. pub close_window_on_invalid_uri_navigation: bool, - pub theme: Option<wry::application::window::Theme>, + pub theme: Option<Theme>, /// Provide a list of ports to use for interactive authentication. /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. @@ -14,9 +17,46 @@ pub struct WebViewOptions { pub clear_browsing_data: bool, } +impl WebViewOptions { + pub fn builder() -> WebViewOptions { + WebViewOptions::default() + } + + pub fn with_window_title(mut self, window_title: impl ToString) -> Self { + self.window_title = window_title.to_string(); + self + } + + pub fn with_close_window_on_invalid_navigation(mut self, close_window: bool) -> Self { + self.close_window_on_invalid_uri_navigation = close_window; + self + } + + pub fn with_theme(mut self, theme: Theme) -> Self { + self.theme = Some(theme); + self + } + + pub fn with_ports(mut self, ports: &[usize]) -> Self { + self.ports = ports.into_iter().cloned().collect(); + self + } + + pub fn with_timeout(mut self, duration: Duration) -> Self { + self.timeout = duration; + self + } + + pub fn with_clear_browsing_data_on_window_close(mut self, clear_browsing_data: bool) -> Self { + self.clear_browsing_data = clear_browsing_data; + self + } +} + impl Default for WebViewOptions { fn default() -> Self { WebViewOptions { + window_title: "Sign In".to_string(), close_window_on_invalid_uri_navigation: true, theme: None, ports: vec![], diff --git a/src/client/graph.rs b/src/client/graph.rs index 6fecc4a0..4c75718a 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -12,6 +12,7 @@ use crate::authentication_method_configurations::{ }; use crate::authentication_methods_policy::AuthenticationMethodsPolicyApiClient; +use crate::api_default_imports::GraphClientConfiguration; use crate::batch::BatchApiClient; use crate::branding::BrandingApiClient; use crate::certificate_based_auth_configuration::{ @@ -45,6 +46,11 @@ use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdA use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; use crate::oauth::{AllowedHostValidator, HostValidator, Token}; +use crate::oauth::{ + AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, + AuthorizationCodeCredential, BearerTokenCredential, ClientAssertionCredential, + ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, +}; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -64,9 +70,7 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; -use graph_http::api_impl::GraphClientConfiguration; -use graph_oauth::identity::{ClientSecretCredential, ConfidentialClientApplication}; -use graph_oauth::oauth::BearerTokenCredential; +use graph_oauth::oauth::OpenIdCredential; use lazy_static::lazy_static; lazy_static! { @@ -85,7 +89,7 @@ pub struct Graph { impl Graph { pub fn new<AT: ToString>(access_token: AT) -> Graph { Graph { - client: Client::new(BearerTokenCredential::new(access_token.to_string())), + client: Client::new(BearerTokenCredential::from(access_token.to_string())), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), } @@ -524,19 +528,19 @@ impl Graph { impl From<&str> for Graph { fn from(token: &str) -> Self { - Graph::from_client_app(BearerTokenCredential::new(token)) + Graph::from_client_app(BearerTokenCredential::from(token.to_string())) } } impl From<String> for Graph { fn from(token: String) -> Self { - Graph::from_client_app(BearerTokenCredential::new(token)) + Graph::from_client_app(BearerTokenCredential::from(token)) } } impl From<&Token> for Graph { fn from(token: &Token) -> Self { - Graph::from_client_app(BearerTokenCredential::new(token.access_token.clone())) + Graph::from_client_app(BearerTokenCredential::from(token.access_token.clone())) } } @@ -550,13 +554,45 @@ impl From<GraphClientConfiguration> for Graph { } } -impl From<ConfidentialClientApplication<ClientSecretCredential>> for Graph { - fn from(value: ConfidentialClientApplication<ClientSecretCredential>) -> Self { - Graph { - client: Client::new(value), - endpoint: PARSED_GRAPH_URL.clone(), - allowed_host_validator: AllowedHostValidator::default(), - } +impl From<&ConfidentialClientApplication<AuthorizationCodeCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<AuthorizationCodeCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<AuthorizationCodeAssertionCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<AuthorizationCodeAssertionCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<AuthorizationCodeCertificateCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<ClientSecretCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<ClientSecretCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<ClientCertificateCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<ClientCertificateCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<ClientAssertionCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<ClientAssertionCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + +impl From<&ConfidentialClientApplication<OpenIdCredential>> for Graph { + fn from(value: &ConfidentialClientApplication<OpenIdCredential>) -> Self { + Graph::from_client_app(value.clone()) } } diff --git a/src/lib.rs b/src/lib.rs index 6d8465c4..215be336 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -242,12 +242,12 @@ pub use graph_http::api_impl::{GraphClientConfiguration, ODataQuery}; /// Reexport of graph-oauth crate. pub mod oauth { - pub use graph_oauth::jwt; + pub use graph_core::identity::ClientApplication; pub use graph_oauth::oauth::*; } pub mod http { - pub use graph_extensions::http::{HttpResponseBuilderExt, HttpResponseExt}; + pub use graph_core::http::{HttpResponseBuilderExt, HttpResponseExt}; pub use graph_http::api_impl::{BodyRead, FileConfig, UploadSession}; pub use graph_http::traits::{ AsyncIterator, ODataDeltaLink, ODataDownloadLink, ODataMetadataLink, ODataNextLink, diff --git a/test-tools/src/lib.rs b/test-tools/src/lib.rs index 9b493e94..ac7eedb2 100644 --- a/test-tools/src/lib.rs +++ b/test-tools/src/lib.rs @@ -6,6 +6,5 @@ extern crate serde_json; extern crate lazy_static; pub mod common; -pub mod oauth; pub mod oauth_request; pub mod support; diff --git a/test-tools/src/oauth.rs b/test-tools/src/oauth.rs deleted file mode 100644 index 8b896db0..00000000 --- a/test-tools/src/oauth.rs +++ /dev/null @@ -1,75 +0,0 @@ -use graph_rs_sdk::oauth::*; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct OAuthTestTool; - -impl OAuthTestTool { - pub fn oauth_contains_credentials(oauth: &mut OAuthSerializer, credentials: &[OAuthParameter]) { - for oac in credentials.iter() { - assert!(oauth.contains(*oac)); - } - } - - pub fn for_each_scope(s: &[String]) { - OAuthTestTool::for_each_fn_scope(OAuthTestTool::join_scopes, s); - OAuthTestTool::for_each_fn_scope(OAuthTestTool::contains_scopes, s); - OAuthTestTool::for_each_fn_scope(OAuthTestTool::remove_scopes, s); - OAuthTestTool::for_each_fn_scope(OAuthTestTool::get_scopes, s); - OAuthTestTool::for_each_fn_scope(OAuthTestTool::clear_scopes, s); - OAuthTestTool::for_each_fn_scope(OAuthTestTool::distinct_scopes, s); - } - - pub fn for_each_fn_scope<F>(mut func: F, scopes: &[String]) - where - F: FnMut(&mut OAuthSerializer, &[String]), - { - let mut oauth = OAuthSerializer::new(); - oauth.extend_scopes(scopes); - func(&mut oauth, scopes) - } - - pub fn join_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - assert_eq!(s.join(" "), oauth.join_scopes(" ")); - } - - pub fn contains_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - for string in s { - assert!(oauth.contains_scope(string.as_str())); - } - } - - pub fn remove_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - for string in s { - oauth.remove_scope(string.as_str()); - assert!(!oauth.contains_scope(string)); - } - } - - pub fn get_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - assert_eq!( - s, - oauth - .get_scopes() - .iter() - .map(|s| s.as_str()) - .collect::<Vec<&str>>() - .as_slice() - ) - } - - pub fn clear_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - OAuthTestTool::join_scopes(oauth, s); - assert!(!oauth.get_scopes().is_empty()); - oauth.clear_scopes(); - assert!(oauth.get_scopes().is_empty()) - } - - pub fn distinct_scopes(oauth: &mut OAuthSerializer, s: &[String]) { - assert_eq!(s.len(), oauth.get_scopes().len()); - let s0 = &s[0]; - oauth.add_scope(s0.as_str()); - assert_eq!(s.len(), oauth.get_scopes().len()); - oauth.extend_scopes(s); - assert_eq!(s.len(), oauth.get_scopes().len()); - } -} diff --git a/tests/oauth_tests.rs b/tests/oauth_tests.rs deleted file mode 100644 index 1a64358e..00000000 --- a/tests/oauth_tests.rs +++ /dev/null @@ -1,156 +0,0 @@ -use graph_oauth::oauth::IntoEnumIterator; -use graph_oauth::oauth::{OAuthParameter, OAuthSerializer}; - -#[test] -fn oauth_parameters_from_credential() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("client_id") - .client_secret("client_secret") - .authorization_url("https://example.com/authorize?") - .token_uri("https://example.com/token?") - .refresh_token_url("https://example.com/token?") - .redirect_uri("https://example.com/redirect?") - .authorization_code("ADSLFJL4L3") - .response_mode("response_mode") - .response_type("response_type") - .state("state") - .grant_type("grant_type") - .nonce("nonce") - .prompt("login") - .session_state("session_state") - .client_assertion("client_assertion") - .client_assertion_type("client_assertion_type") - .code_verifier("code_verifier") - .login_hint("login_hint") - .domain_hint("domain_hint") - .resource("resource") - .logout_url("https://example.com/logout?") - .post_logout_redirect_uri("https://example.com/redirect?"); - - OAuthParameter::iter().for_each(|credential| { - if oauth.contains(credential) { - match credential { - OAuthParameter::ClientId => { - assert_eq!(oauth.get(credential), Some("client_id".into())) - } - OAuthParameter::ClientSecret => { - assert_eq!(oauth.get(credential), Some("client_secret".into())) - } - OAuthParameter::AuthorizationUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/authorize?".into()) - ), - OAuthParameter::TokenUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/token?".into()) - ), - OAuthParameter::RefreshTokenUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/token?".into()) - ), - OAuthParameter::RedirectUri => assert_eq!( - oauth.get(credential), - Some("https://example.com/redirect?".into()) - ), - OAuthParameter::AuthorizationCode => { - assert_eq!(oauth.get(credential), Some("ADSLFJL4L3".into())) - } - OAuthParameter::ResponseMode => { - assert_eq!(oauth.get(credential), Some("response_mode".into())) - } - OAuthParameter::ResponseType => { - assert_eq!(oauth.get(credential), Some("response_type".into())) - } - OAuthParameter::State => assert_eq!(oauth.get(credential), Some("state".into())), - OAuthParameter::GrantType => { - assert_eq!(oauth.get(credential), Some("grant_type".into())) - } - OAuthParameter::Nonce => assert_eq!(oauth.get(credential), Some("nonce".into())), - OAuthParameter::LogoutURL => assert_eq!( - oauth.get(credential), - Some("https://example.com/logout?".into()) - ), - OAuthParameter::PostLogoutRedirectURI => assert_eq!( - oauth.get(credential), - Some("https://example.com/redirect?".into()) - ), - OAuthParameter::Prompt => assert_eq!(oauth.get(credential), Some("login".into())), - OAuthParameter::SessionState => { - assert_eq!(oauth.get(credential), Some("session_state".into())) - } - OAuthParameter::ClientAssertion => { - assert_eq!(oauth.get(credential), Some("client_assertion".into())) - } - OAuthParameter::ClientAssertionType => { - assert_eq!(oauth.get(credential), Some("client_assertion_type".into())) - } - OAuthParameter::CodeVerifier => { - assert_eq!(oauth.get(credential), Some("code_verifier".into())) - } - OAuthParameter::Resource => { - assert_eq!(oauth.get(credential), Some("resource".into())) - } - _ => {} - } - } - }); -} - -#[test] -fn remove_credential() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .client_secret("CLDIE3F") - .authorization_url("https://www.example.com/authorize?") - .refresh_token_url("https://www.example.com/token?") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - assert!(oauth.get(OAuthParameter::ClientId).is_some()); - oauth.remove(OAuthParameter::ClientId); - assert!(oauth.get(OAuthParameter::ClientId).is_none()); - oauth.client_id("client_id"); - assert!(oauth.get(OAuthParameter::ClientId).is_some()); - - assert!(oauth.get(OAuthParameter::RedirectUri).is_some()); - oauth.remove(OAuthParameter::RedirectUri); - assert!(oauth.get(OAuthParameter::RedirectUri).is_none()); -} - -#[test] -fn setters() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("client_id") - .client_secret("client_secret") - .authorization_url("https://example.com/authorize") - .refresh_token_url("https://example.com/token") - .token_uri("https://example.com/token") - .redirect_uri("https://example.com/redirect") - .authorization_code("access_code"); - - let test_setter = |c: OAuthParameter, s: &str| { - let result = oauth.get(c); - assert!(result.is_some()); - assert!(result.is_some()); - assert_eq!(result.unwrap(), s); - }; - - test_setter(OAuthParameter::ClientId, "client_id"); - test_setter(OAuthParameter::ClientSecret, "client_secret"); - test_setter( - OAuthParameter::AuthorizationUrl, - "https://example.com/authorize", - ); - test_setter(OAuthParameter::RefreshTokenUrl, "https://example.com/token"); - test_setter(OAuthParameter::TokenUrl, "https://example.com/token"); - test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); - test_setter(OAuthParameter::AuthorizationCode, "access_code"); -} diff --git a/tests/token_cache_tests.rs b/tests/token_cache_tests.rs index 63d1e26e..ea7abc14 100644 --- a/tests/token_cache_tests.rs +++ b/tests/token_cache_tests.rs @@ -1,4 +1,4 @@ -use graph_extensions::cache::TokenCacheStore; +use graph_core::cache::TokenCache; use std::thread; use std::time::Duration; use test_tools::oauth_request::OAuthTestClient; From e3e272899c537dcafa6ac10b129c15dcc60f8208 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 30 Oct 2023 09:10:23 -0400 Subject: [PATCH 052/118] Update how queries are serialized and update README.md --- README.md | 30 ++++++-------- graph-oauth/src/auth.rs | 16 ++++---- graph-oauth/src/identity/authority.rs | 36 +++++++++++++++-- .../src/identity/credentials/app_config.rs | 7 +++- .../auth_code_authorization_url.rs | 12 ++---- .../client_certificate_credential.rs | 2 +- .../client_credentials_authorization_url.rs | 40 +++++-------------- .../legacy/code_flow_authorization_url.rs | 7 +--- .../credentials/legacy/implicit_credential.rs | 6 +-- .../legacy/token_flow_authorization_url.rs | 6 +-- .../credentials/open_id_authorization_url.rs | 6 +-- 11 files changed, 83 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index e8f434ab..545a393c 100644 --- a/README.md +++ b/README.md @@ -1029,6 +1029,7 @@ The following flows from the Microsoft Identity Platform are supported: - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) - [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +- [Client Credentials With Certificate](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) You can use the url builders for those flows that require an authorization code using a redirect after sign in you can use @@ -1093,17 +1094,13 @@ use graph_rs_sdk::{ oauth::ConfidentialClientApplication, Graph }; -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static TENANT_ID: &str = "<TENANT_ID>"; - -pub async fn get_graph_client() -> Graph { - let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_tenant(TENANT_ID) +pub async fn get_graph_client(tenant: &str, client_id: &str, client_secret: &str) -> Graph { + let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) + .with_client_secret(client_secret) + .with_tenant(tenant) .build(); - Graph::from(confidential_client_application) + Graph::from(&confidential_client_application) } ``` @@ -1121,18 +1118,15 @@ Tokens will still be automatically refreshed as this flow does not require using a new access token. ```rust -async fn authenticate() { +async fn authenticate(client_id: &str, tenant: &str, redirect_uri: &str) { let scope = vec!["offline_access"]; - let mut credential_builder = ConfidentialClientApplication::builder(CLIENT_ID) + let mut credential_builder = ConfidentialClientApplication::builder(client_id) .auth_code_url_builder() - .interactive_authentication(None) // Open web view for interactive authentication sign in - .unwrap(); + .with_tenant(tenant) + .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .url(); // ... add any other parameters you need - - let confidential_client = credential_builder.with_client_secret(CLIENT_SECRET) - .build(); - - let client = Graph::from(&confidential_client); } ``` diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index e94f9c4c..03c8df72 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -827,33 +827,33 @@ impl OAuthSerializer { &mut self, optional_fields: Vec<OAuthParameter>, required_fields: Vec<OAuthParameter>, - encoder: &mut Serializer<String>, - ) -> IdentityResult<()> { + ) -> IdentityResult<String> { + let mut serializer = Serializer::new(String::new()); for parameter in required_fields { if parameter.alias().eq("scope") { if self.scopes.is_empty() { - return AuthorizationFailure::result::<()>(parameter.alias()); + return AuthorizationFailure::result::<String>(parameter.alias()); } else { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); + serializer.append_pair("scope", self.join_scopes(" ").as_str()); } } else { let value = self .get(parameter) .ok_or(AuthorizationFailure::required(parameter))?; - encoder.append_pair(parameter.alias(), value.as_str()); + serializer.append_pair(parameter.alias(), value.as_str()); } } for parameter in optional_fields { if parameter.alias().eq("scope") && !self.scopes.is_empty() { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); + serializer.append_pair("scope", self.join_scopes(" ").as_str()); } else if let Some(val) = self.get(parameter) { - encoder.append_pair(parameter.alias(), val.as_str()); + serializer.append_pair(parameter.alias(), val.as_str()); } } - Ok(()) + Ok(serializer.finish()) } pub fn params(&mut self, pairs: Vec<OAuthParameter>) -> GraphResult<HashMap<String, String>> { diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 6d019ae0..bdcb711b 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -1,4 +1,4 @@ -use url::Url; +use url::{ParseError, Url}; /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). /// Maps to the instance url string. @@ -6,8 +6,6 @@ use url::Url; Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, )] pub enum AzureCloudInstance { - // Custom Value communicating that the AzureCloudInstance. - //Custom(String), /// Microsoft Azure public cloud. Maps to https://login.microsoftonline.com #[default] AzurePublic, @@ -45,6 +43,38 @@ impl TryFrom<AzureCloudInstance> for Url { } impl AzureCloudInstance { + pub fn auth_uri(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!( + "{}/{}/oauth2/v2.0/authorize", + self.as_ref(), + authority.as_ref() + )) + } + + pub fn token_uri(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!( + "{}/{}/oauth2/v2.0/token", + self.as_ref(), + authority.as_ref() + )) + } + + pub fn admin_consent_uri(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!( + "{}/{}/adminconsent", + self.as_ref(), + authority.as_ref() + )) + } + + pub fn device_code_uri(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!( + "{}/{}/oauth2/v2.0/devicecode", + self.as_ref(), + authority.as_ref() + )) + } + pub fn default_microsoft_graph_scope(&self) -> &'static str { "https://graph.microsoft.com/.default" } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 9f6d65d4..0bed69f4 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -102,10 +102,15 @@ impl AppConfig { } }; + let authority = tenant_id + .clone() + .map(|tenant| Authority::TenantId(tenant)) + .unwrap_or_default(); + AppConfig { tenant_id, client_id, - authority: Default::default(), + authority, azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index c8432c93..615f3de3 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -372,7 +372,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { } let mut encoder = Serializer::new(String::new()); - serializer.encode_query( + let query = serializer.encode_query( vec![ OAuthParameter::ResponseMode, OAuthParameter::State, @@ -389,15 +389,11 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { OAuthParameter::RedirectUri, OAuthParameter::Scope, ], - &mut encoder, )?; - let authorization_url = serializer - .get(OAuthParameter::AuthorizationUrl) - .ok_or(AF::msg_internal_err("authorization_url"))?; - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); - Ok(url) + let mut uri = azure_cloud_instance.auth_uri(&self.app_config.authority)?; + uri.set_query(Some(query.as_str())); + Ok(uri) } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 4e4d1225..3481eaf9 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -84,7 +84,7 @@ impl ClientCertificateCredential { impl Debug for ClientCertificateCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ClientAssertionCredential") + f.debug_struct("ClientCertificateCredential") .field("app_config", &self.app_config) .field("scope", &self.scope) .finish() diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index ed5a0266..fc751400 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -16,9 +16,9 @@ pub struct ClientCredentialsAuthorizationUrlParameters { } impl ClientCredentialsAuthorizationUrlParameters { - pub fn new<T: AsRef<str>, U: IntoUrl>( - client_id: T, - redirect_uri: U, + pub fn new( + client_id: impl AsRef<str>, + redirect_uri: impl IntoUrl, ) -> IdentityResult<ClientCredentialsAuthorizationUrlParameters> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; @@ -64,31 +64,13 @@ impl ClientCredentialsAuthorizationUrlParameters { serializer.state(state.as_ref()); } - serializer.authority_admin_consent(azure_cloud_instance, &self.app_config.authority); - - let mut encoder = Serializer::new(String::new()); - serializer.form_encode_credentials( - vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::State, - ], - &mut encoder, - ); - - let mut url = Url::parse( - serializer - .get(OAuthParameter::AuthorizationUrl) - .ok_or(AuthorizationFailure::required( - OAuthParameter::AuthorizationUrl.alias(), - ))? - .as_str(), - ) - .or(AuthorizationFailure::result( - OAuthParameter::AuthorizationUrl.alias(), - ))?; - url.set_query(Some(encoder.finish().as_str())); - Ok(url) + let mut uri = azure_cloud_instance.admin_consent_uri(&self.app_config.authority)?; + let query = serializer.encode_query( + vec![OAuthParameter::State], + vec![OAuthParameter::ClientId, OAuthParameter::RedirectUri], + )?; + uri.set_query(Some(query.as_str())); + Ok(uri) } } @@ -107,7 +89,7 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { } } - pub fn new_with_app_config(app_config: AppConfig) -> Self { + pub(crate) fn new_with_app_config(app_config: AppConfig) -> Self { Self { parameters: ClientCredentialsAuthorizationUrlParameters { app_config, diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index e3351b58..5caae5cb 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -69,9 +69,7 @@ impl CodeFlowAuthorizationUrl { .legacy_authority() .response_type(self.response_type.clone()); - let mut encoder = Serializer::new(String::new()); - - serializer.encode_query( + let query = serializer.encode_query( vec![], vec![ OAuthParameter::ClientId, @@ -79,12 +77,11 @@ impl CodeFlowAuthorizationUrl { OAuthParameter::Scope, OAuthParameter::ResponseType, ], - &mut encoder, )?; if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); + url.set_query(Some(query.as_str())); Ok(url) } else { AuthorizationFailure::msg_result("authorization_url", "Internal Error") diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index b25e4926..105b2eed 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -188,8 +188,7 @@ impl ImplicitCredential { serializer.login_hint(login_hint.as_str()); } - let mut encoder = Serializer::new(String::new()); - serializer.encode_query( + let query = serializer.encode_query( vec![ OAuthParameter::RedirectUri, OAuthParameter::ResponseMode, @@ -204,12 +203,11 @@ impl ImplicitCredential { OAuthParameter::Scope, OAuthParameter::Nonce, ], - &mut encoder, )?; if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); + url.set_query(Some(query.as_str())); Ok(url) } else { AuthorizationFailure::msg_result("authorization_url", "Internal Error") diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index fc74d03f..daaefbfe 100644 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -56,8 +56,7 @@ impl TokenFlowAuthorizationUrl { .legacy_authority() .response_type(self.response_type.clone()); - let mut encoder = Serializer::new(String::new()); - serializer.encode_query( + let query = serializer.encode_query( vec![], vec![ OAuthParameter::ClientId, @@ -65,12 +64,11 @@ impl TokenFlowAuthorizationUrl { OAuthParameter::Scope, OAuthParameter::ResponseType, ], - &mut encoder, )?; if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); + url.set_query(Some(query.as_str())); Ok(url) } else { AF::msg_internal_result("authorization_url") diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 842a6cf2..6f54d9ed 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -288,8 +288,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer.login_hint(login_hint.as_str()); } - let mut encoder = Serializer::new(String::new()); - serializer.encode_query( + let query = serializer.encode_query( vec![ OAuthParameter::ResponseMode, OAuthParameter::RedirectUri, @@ -304,14 +303,13 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { OAuthParameter::Scope, OAuthParameter::Nonce, ], - &mut encoder, )?; let authorization_url = serializer .get(OAuthParameter::AuthorizationUrl) .ok_or(AF::msg_err("authorization_url", "Internal Error"))?; let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(encoder.finish().as_str())); + url.set_query(Some(query.as_str())); Ok(url) } } From 8fd3cb334a4e3c0bf59f84102feaf004337a18fa Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 1 Nov 2023 08:28:31 -0400 Subject: [PATCH 053/118] Add interactive mode to device code and implement time out of web view --- Cargo.toml | 2 - graph-http/Cargo.toml | 2 +- graph-oauth/src/auth.rs | 1 - graph-oauth/src/discovery/graph_discovery.rs | 77 ---------- graph-oauth/src/discovery/jwt_keys.rs | 86 ----------- graph-oauth/src/discovery/mod.rs | 2 - .../auth_code_authorization_url.rs | 29 +--- .../credentials/device_code_credential.rs | 144 +++++++++++++++++- .../credentials/environment_credential.rs | 12 -- graph-oauth/src/identity/credentials/mod.rs | 4 - .../credentials/token_credential_options.rs | 11 -- .../src/identity/credentials/token_request.rs | 76 --------- graph-oauth/src/lib.rs | 1 - graph-oauth/src/web/interactive_web_view.rs | 7 +- graph-oauth/src/web/web_view_options.rs | 11 +- 15 files changed, 155 insertions(+), 310 deletions(-) delete mode 100644 graph-oauth/src/discovery/graph_discovery.rs delete mode 100644 graph-oauth/src/discovery/jwt_keys.rs delete mode 100644 graph-oauth/src/discovery/mod.rs delete mode 100644 graph-oauth/src/identity/credentials/token_credential_options.rs delete mode 100644 graph-oauth/src/identity/credentials/token_request.rs diff --git a/Cargo.toml b/Cargo.toml index 1058ade2..7f96d054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,8 +70,6 @@ anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.4" from_as = "0.2.0" -actix = "0.13.0" -actix-rt = "2.8.0" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 55e1b4b0..1be15646 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7.1" thiserror = "1" -tokio = { version = "1.27.0", features = ["full"] } +tokio = { version = "1.27.0", features = ["full", "tracing"] } url = { version = "2", features = ["serde"] } graph-error = { path = "../graph-error" } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 03c8df72..7cd8e479 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -3,7 +3,6 @@ use std::collections::{BTreeSet, HashMap}; use std::default::Default; use std::fmt; -use base64::Engine; use url::form_urlencoded::Serializer; use url::Url; diff --git a/graph-oauth/src/discovery/graph_discovery.rs b/graph-oauth/src/discovery/graph_discovery.rs deleted file mode 100644 index e39f86a3..00000000 --- a/graph-oauth/src/discovery/graph_discovery.rs +++ /dev/null @@ -1,77 +0,0 @@ -static LOGIN_LIVE_HOST: &str = "https://login.live.com"; -static MICROSOFT_ONLINE_HOST: &str = "https://login.microsoftonline.com"; -static OPEN_ID_PATH: &str = ".well-known/openid-configuration"; - -#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct MicrosoftSigningKeysV1 { - pub issuer: String, - pub authorization_endpoint: String, - pub token_endpoint: String, - pub token_endpoint_auth_methods_supported: Vec<String>, - pub jwks_uri: String, - pub response_types_supported: Vec<String>, - pub response_modes_supported: Vec<String>, - pub subject_types_supported: Vec<String>, - pub scopes_supported: Vec<String>, - pub id_token_signing_alg_values_supported: Vec<String>, - pub claims_supported: Vec<String>, - pub request_uri_parameter_supported: bool, - pub end_session_endpoint: String, - pub frontchannel_logout_supported: bool, - pub http_logout_supported: bool, -} - -#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize)] -pub struct MicrosoftSigningKeysV2 { - pub authorization_endpoint: String, - pub token_endpoint: String, - pub token_endpoint_auth_methods_supported: Vec<String>, - pub jwks_uri: String, - pub response_modes_supported: Vec<String>, - pub subject_types_supported: Vec<String>, - pub id_token_signing_alg_values_supported: Vec<String>, - pub http_logout_supported: bool, - pub frontchannel_logout_supported: bool, - pub end_session_endpoint: String, - pub response_types_supported: Vec<String>, - pub scopes_supported: Vec<String>, - pub issuer: String, - pub claims_supported: Vec<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub microsoft_multi_refresh_token: Option<bool>, - #[serde(skip_serializing_if = "Option::is_none")] - pub check_session_iframe: Option<String>, - pub userinfo_endpoint: String, - pub tenant_region_scope: Option<String>, - pub cloud_instance_name: String, - pub cloud_graph_host_name: String, - pub msgraph_host: String, - pub rbac_url: String, -} - -pub enum SigningKeys { - V1, - V2, - Tenant(String), -} - -impl SigningKeys { - /// Get the URL for the public keys used by the Microsoft identity platform - /// to sign security tokens. - /// - /// # Example - /// ``` - /// # use graph_oauth::oauth::graph_discovery::SigningKeys; - /// let url = SigningKeys::V1.url(); - /// println!("{}", url); - /// ``` - pub fn url(&self) -> String { - match self { - SigningKeys::V1 => format!("{LOGIN_LIVE_HOST}/{OPEN_ID_PATH}"), - SigningKeys::V2 => format!("{MICROSOFT_ONLINE_HOST}/common/v2.0/{OPEN_ID_PATH}"), - SigningKeys::Tenant(tenant) => { - format!("{MICROSOFT_ONLINE_HOST}/{tenant}/v2.0/{OPEN_ID_PATH}") - } - } - } -} diff --git a/graph-oauth/src/discovery/jwt_keys.rs b/graph-oauth/src/discovery/jwt_keys.rs deleted file mode 100644 index e8bca78b..00000000 --- a/graph-oauth/src/discovery/jwt_keys.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::collections::HashMap; - -use graph_error::GraphResult; - -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct Keys { - #[serde(skip_serializing_if = "Option::is_none")] - pub kty: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "use")] - pub _use: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub kid: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub x5t: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub n: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub e: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub x5c: Option<Vec<String>>, -} - -impl Keys { - pub fn to_map(&self) -> HashMap<String, String> { - let mut hashmap: HashMap<String, String> = HashMap::new(); - hashmap.insert("kty".into(), self.kty.clone().unwrap_or_default()); - hashmap.insert("use".into(), self._use.clone().unwrap_or_default()); - hashmap.insert("kid".into(), self.kid.clone().unwrap_or_default()); - hashmap.insert("x5t".into(), self.x5t.clone().unwrap_or_default()); - hashmap.insert("n".into(), self.n.clone().unwrap_or_default()); - hashmap.insert("e".into(), self.e.clone().unwrap_or_default()); - if let Some(x5) = &self.x5c { - hashmap.insert("x5c".into(), x5[0].to_string()); - } - hashmap - } -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct JWTKeys { - keys: Vec<Keys>, -} - -impl JWTKeys { - pub fn discovery() -> GraphResult<JWTKeys> { - let client = reqwest::blocking::Client::new(); - let response = client - .get("https://login.microsoftonline.com/common/discovery/keys") - .send()?; - let keys: JWTKeys = response.json()?; - Ok(keys) - } - - pub async fn async_discovery() -> GraphResult<JWTKeys> { - let client = reqwest::Client::new(); - let response = client - .get("https://login.microsoftonline.com/common/discovery/keys") - .send() - .await?; - let keys: JWTKeys = response.json().await?; - Ok(keys) - } - - pub fn keys(&self) -> Vec<Keys> { - self.keys.to_vec() - } - - pub fn key_map(&mut self) -> Vec<HashMap<String, String>> { - let mut vec: Vec<HashMap<String, String>> = Vec::new(); - for key in self.keys.iter() { - vec.push(key.to_map()); - } - - vec - } -} - -impl IntoIterator for JWTKeys { - type Item = Keys; - type IntoIter = std::vec::IntoIter<Self::Item>; - - fn into_iter(self) -> Self::IntoIter { - self.keys.into_iter() - } -} diff --git a/graph-oauth/src/discovery/mod.rs b/graph-oauth/src/discovery/mod.rs deleted file mode 100644 index 8271586d..00000000 --- a/graph-oauth/src/discovery/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod graph_discovery; -pub mod jwt_keys; diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 615f3de3..9a486365 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -3,6 +3,7 @@ use std::fmt::{Debug, Formatter}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; +use time::{Duration, Instant}; use url::form_urlencoded::Serializer; use url::Url; use uuid::Uuid; @@ -194,6 +195,7 @@ impl AuthCodeAuthorizationUrlParameters { let receiver = self.interactive_authentication(interactive_web_view_options)?; let mut iter = receiver.try_iter(); let mut next = iter.next(); + while next.is_none() { next = iter.next(); } @@ -247,7 +249,10 @@ pub(crate) mod web_view_authenticator { &self, interactive_web_view_options: Option<WebViewOptions>, ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { - let uri = self.authorization_url()?; + let uri = self + .app_config + .azure_cloud_instance + .auth_uri(&self.app_config.authority)?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let _timeout = web_view_options.timeout; @@ -371,7 +376,6 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { serializer.code_challenge_method(code_challenge_method.as_str()); } - let mut encoder = Serializer::new(String::new()); let query = serializer.encode_query( vec![ OAuthParameter::ResponseMode, @@ -450,27 +454,6 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - /* - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.parameters.app_config.client_id = - Uuid::try_parse(client_id.as_ref()).expect("Invalid Client Id - Must be a Uuid "); - self - } - - /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] - pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - let tenant_id = tenant.as_ref(); - self.parameters.app_config.tenant_id = Some(tenant_id.to_owned()); - self.parameters.app_config.authority = Authority::TenantId(tenant_id.to_owned()); - self - } - - pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.parameters.app_config.authority = authority.into(); - self - } - */ - /// Default is code. Must include code for the authorization code flow. /// Can also include id_token or token if using the hybrid flow. pub fn with_response_type<I: IntoIterator<Item = ResponseType>>( diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index ee06a952..54686e88 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -1,9 +1,11 @@ +use async_trait::async_trait; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::ops::Add; use std::str::FromStr; use std::time::Duration; +use graph_core::cache::{InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; @@ -19,9 +21,12 @@ use graph_error::{ use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, DeviceCode, ForceTokenRefresh, PollDeviceCodeType, - PublicClientApplication, TokenCredentialExecutor, + Authority, AuthorizationQueryResponse, AzureCloudInstance, DeviceCode, ForceTokenRefresh, + PollDeviceCodeType, PublicClientApplication, TokenCredentialExecutor, }; +use crate::oauth::Token; +use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use crate::web::{InteractiveAuthenticator, InteractiveWebView}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -54,6 +59,7 @@ pub struct DeviceCodeCredential { /// the token for during token redemption. pub(crate) scope: Vec<String>, serializer: OAuthSerializer, + token_cache: InMemoryCacheStore<Token>, } impl DeviceCodeCredential { @@ -68,17 +74,16 @@ impl DeviceCodeCredential { device_code: Some(device_code.as_ref().to_owned()), scope: scope.into_iter().map(|s| s.to_string()).collect(), serializer: Default::default(), + token_cache: Default::default(), } } pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.device_code = None; self.refresh_token = Some(refresh_token.as_ref().to_owned()); self } pub fn with_device_code<T: AsRef<str>>(&mut self, device_code: T) -> &mut Self { - self.refresh_token = None; self.device_code = Some(device_code.as_ref().to_owned()); self } @@ -96,6 +101,50 @@ impl Debug for DeviceCodeCredential { .finish() } } + +#[async_trait] +impl TokenCache for DeviceCodeCredential { + type Token = Token; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } else { + Ok(token) + } + } else { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } else { + Ok(token.clone()) + } + } else { + let response = self.execute_async().await?; + let msal_token: Token = response.json().await?; + self.token_cache.store(cache_id, msal_token.clone()); + Ok(msal_token) + } + } +} + impl TokenCredentialExecutor for DeviceCodeCredential { fn uri(&mut self) -> IdentityResult<Url> { let azure_cloud_instance = self.azure_cloud_instance(); @@ -208,6 +257,7 @@ impl DeviceCodeCredentialBuilder { device_code: None, scope: vec![], serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -223,6 +273,7 @@ impl DeviceCodeCredentialBuilder { device_code: Some(device_code.as_ref().to_owned()), scope: vec![], serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -255,6 +306,7 @@ impl From<&DeviceCode> for DeviceCodeCredentialBuilder { device_code: Some(value.device_code.clone()), scope: vec![], serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -273,6 +325,7 @@ impl DeviceCodePollingExecutor { device_code: None, scope: vec![], serializer: Default::default(), + token_cache: Default::default(), }, } } @@ -282,6 +335,53 @@ impl DeviceCodePollingExecutor { self } + pub fn interactive_webview_authentication( + &self, + options: Option<WebViewOptions>, + ) -> anyhow::Result<AuthorizationQueryResponse> { + let receiver = self.credential.interactive_authentication(options)?; + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + + while next.is_none() { + next = iter.next(); + } + + return match next { + None => Err(anyhow::anyhow!("Unknown")), + Some(auth_event) => { + match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::TimedOut(duration) => { + Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => { + Err(anyhow::anyhow!("CloseRequested")) + } + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) + } + } + } + } + } + }; + } + pub fn poll(&mut self) -> AuthExecutionResult<std::sync::mpsc::Receiver<JsonHttpResponse>> { let (sender, receiver) = std::sync::mpsc::channel(); @@ -437,6 +537,42 @@ impl DeviceCodePollingExecutor { } } +// #[cfg(feature = "interactive-auth")] +pub(crate) mod web_view_authenticator { + use crate::oauth::DeviceCodeCredential; + use crate::web::{ + InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, + }; + use graph_error::IdentityResult; + + impl InteractiveAuthenticator for DeviceCodeCredential { + fn interactive_authentication( + &self, + interactive_web_view_options: Option<WebViewOptions>, + ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { + let uri = self + .app_config + .azure_cloud_instance + .auth_uri(&self.app_config.authority)?; + let redirect_uri = self.app_config.redirect_uri.clone().unwrap(); + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let (sender, receiver) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + InteractiveWebView::interactive_authentication( + uri, + vec![redirect_uri], + web_view_options, + sender, + ) + .unwrap(); + }); + + Ok(receiver) + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index 6faf7ea0..c78a9c7f 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -140,18 +140,6 @@ impl EnvironmentCredential { } } -/* -impl AuthorizationSerializer for EnvironmentCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { - self.credential.uri() - } - - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - self.credential.form_urlencode() - } -} - */ - impl TokenCredentialExecutor for EnvironmentCredential { fn uri(&mut self) -> IdentityResult<Url> { self.credential.uri() diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 06a45721..6be7d44e 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -23,8 +23,6 @@ pub use resource_owner_password_credential::*; pub use response_mode::*; pub use response_type::*; pub use token_credential_executor::*; -pub use token_credential_options::*; -pub use token_request::*; #[cfg(feature = "openssl")] pub use x509_certificate::*; @@ -58,8 +56,6 @@ mod resource_owner_password_credential; mod response_mode; mod response_type; mod token_credential_executor; -mod token_credential_options; -mod token_request; #[cfg(feature = "openssl")] mod x509_certificate; diff --git a/graph-oauth/src/identity/credentials/token_credential_options.rs b/graph-oauth/src/identity/credentials/token_credential_options.rs deleted file mode 100644 index b250c87c..00000000 --- a/graph-oauth/src/identity/credentials/token_credential_options.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct TokenCredentialOptions { - /// Specifies if the token request will ignore the access token in the token cache - /// and will attempt to acquire a new access token. - pub force_refresh: bool, - - /// Enables to override the tenant/account for which to get a token. - /// This is useful in multi-tenant apps in the cases where a given user account is a guest - /// in other tenants, and you want to acquire tokens for a specific tenant. - pub tenant: Option<String>, -} diff --git a/graph-oauth/src/identity/credentials/token_request.rs b/graph-oauth/src/identity/credentials/token_request.rs deleted file mode 100644 index 2eccbfb3..00000000 --- a/graph-oauth/src/identity/credentials/token_request.rs +++ /dev/null @@ -1,76 +0,0 @@ -use async_trait::async_trait; -use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE}; -use reqwest::tls::Version; -use reqwest::ClientBuilder; - -use crate::identity::AzureCloudInstance; -use crate::oauth::AuthorizationSerializer; - -#[async_trait] -pub trait TokenRequest: AuthorizationSerializer { - fn azure_cloud_instance(&self) -> AzureCloudInstance; - - fn get_token(&mut self) -> anyhow::Result<reqwest::blocking::Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.uri(&azure_cloud_instance)?; - - let form = self.form_urlencode()?; - let http_client = reqwest::blocking::ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 - let basic_auth = self.basic_auth(); - if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client - .post(uri) - .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) - .send()?) - } else { - Ok(http_client.post(uri).form(&form).send()?) - } - } - - async fn get_token_async(&mut self) -> anyhow::Result<reqwest::Response> { - let azure_cloud_instance = self.azure_cloud_instance(); - let uri = self.uri(&azure_cloud_instance)?; - - let form = self.form_urlencode()?; - let http_client = ClientBuilder::new() - .min_tls_version(Version::TLS_1_2) - .https_only(true) - .build()?; - let mut headers = HeaderMap::new(); - headers.insert( - CONTENT_TYPE, - HeaderValue::from_static("application/x-www-form-urlencoded"), - ); - - // https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1 - let basic_auth = self.basic_auth(); - if let Some((client_identifier, secret)) = basic_auth { - Ok(http_client - .post(uri) - .basic_auth(client_identifier, Some(secret)) - .headers(headers) - .form(&form) - .send() - .await?) - } else { - Ok(http_client - .post(uri) - .headers(headers) - .form(&form) - .send() - .await?) - } - } -} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 24c639e7..1a32de1c 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -57,7 +57,6 @@ extern crate serde; extern crate strum; pub(crate) mod auth; -mod discovery; pub mod jwt; mod oauth_error; diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 3345cb26..de4bf013 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,7 +1,4 @@ -use anyhow::Context; -use std::sync::mpsc::SendError; -use std::time::{Duration, Instant}; -use tracing::instrument::WithSubscriber; +use std::time::Duration; use url::Url; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; @@ -162,7 +159,7 @@ impl InteractiveWebView { .build()?; event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; + *control_flow = ControlFlow::WaitUntil(options.timeout); match event { Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index eb9220b7..dc6f7202 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -1,4 +1,5 @@ -use std::time::Duration; +use std::ops::Add; +use std::time::{Duration, Instant}; pub use wry::application::window::Theme; @@ -13,7 +14,7 @@ pub struct WebViewOptions { /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. pub ports: Vec<usize>, - pub timeout: Duration, + pub timeout: Instant, pub clear_browsing_data: bool, } @@ -42,8 +43,8 @@ impl WebViewOptions { self } - pub fn with_timeout(mut self, duration: Duration) -> Self { - self.timeout = duration; + pub fn with_timeout(mut self, instant: Instant) -> Self { + self.timeout = instant; self } @@ -61,7 +62,7 @@ impl Default for WebViewOptions { theme: None, ports: vec![], // 10 Minutes default timeout - timeout: Duration::from_secs(10 * 60), + timeout: Instant::now().add(Duration::from_secs(10 * 60)), clear_browsing_data: false, } } From 477ed9000a21a003c09570e4571e2ad2ceb0bec1 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 3 Nov 2023 02:49:27 -0400 Subject: [PATCH 054/118] Clean up credential executor and update token cache impl for those that have refresh tokens --- .../customize_webview.rs | 21 ++ examples/interactive_authentication/main.rs | 1 + .../interactive_authentication/web_view.rs | 17 +- .../auth_code_grant/auth_code_grant_pkce.rs | 2 +- examples/oauth/getting_tokens_manually.rs | 38 ++++ examples/oauth/main.rs | 59 ++---- examples/oauth/openid/openid.rs | 53 ++--- examples/oauth_authorization_url/main.rs | 2 +- .../oauth_authorization_url/openid_connect.rs | 35 ++-- graph-core/src/cache/in_memory_cache_store.rs | 7 + graph-http/src/client.rs | 4 +- graph-oauth/src/auth.rs | 2 +- graph-oauth/src/identity/authority.rs | 2 + ...uest.rs => authorization_request_parts.rs} | 0 .../src/identity/credentials/app_config.rs | 70 ++++--- .../auth_code_authorization_url.rs | 64 +++--- ...authorization_code_assertion_credential.rs | 143 +++++++++---- ...thorization_code_certificate_credential.rs | 141 +++++++++---- .../authorization_code_credential.rs | 28 +-- .../credentials/bearer_token_credential.rs | 1 - .../client_assertion_credential.rs | 119 +++++------ .../client_certificate_credential.rs | 121 +++++------ .../client_credentials_authorization_url.rs | 2 +- .../credentials/client_secret_credential.rs | 17 +- .../confidential_client_application.rs | 56 +++-- .../credentials/device_code_credential.rs | 192 +++++++++++------- .../credentials/environment_credential.rs | 78 +------ .../legacy/code_flow_authorization_url.rs | 1 - .../credentials/legacy/implicit_credential.rs | 4 +- .../legacy/token_flow_authorization_url.rs | 1 - .../credentials/open_id_authorization_url.rs | 2 +- .../credentials/open_id_credential.rs | 60 +++--- .../src/identity/credentials/prompt.rs | 11 +- .../resource_owner_password_credential.rs | 26 +-- .../credentials/token_credential_executor.rs | 10 +- graph-oauth/src/identity/mod.rs | 4 +- graph-oauth/src/identity/token_validator.rs | 3 +- .../src/web/interactive_authenticator.rs | 6 +- graph-oauth/src/web/interactive_web_view.rs | 45 +++- graph-oauth/src/web/web_view_options.rs | 4 +- 40 files changed, 755 insertions(+), 697 deletions(-) create mode 100644 examples/interactive_authentication/customize_webview.rs create mode 100644 examples/oauth/getting_tokens_manually.rs rename graph-oauth/src/identity/{authorization_request.rs => authorization_request_parts.rs} (100%) diff --git a/examples/interactive_authentication/customize_webview.rs b/examples/interactive_authentication/customize_webview.rs new file mode 100644 index 00000000..6b0b73b6 --- /dev/null +++ b/examples/interactive_authentication/customize_webview.rs @@ -0,0 +1,21 @@ +use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use std::ops::Add; +use std::time::{Duration, Instant}; + +fn get_webview_options() -> WebViewOptions { + WebViewOptions::builder() + .with_window_title("Sign In") + .with_theme(Theme::Dark) + .with_close_window_on_invalid_navigation(true) + .with_timeout(Instant::now().add(Duration::from_secs(120))) +} + +async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .interactive_authentication(Some(get_webview_options())) + .unwrap(); +} diff --git a/examples/interactive_authentication/main.rs b/examples/interactive_authentication/main.rs index 22c02c7d..918b4e82 100644 --- a/examples/interactive_authentication/main.rs +++ b/examples/interactive_authentication/main.rs @@ -1,5 +1,6 @@ #![allow(dead_code, unused, unused_imports)] +mod customize_webview; mod web_view; fn main() {} diff --git a/examples/interactive_authentication/web_view.rs b/examples/interactive_authentication/web_view.rs index d1fef07c..da8a9507 100644 --- a/examples/interactive_authentication/web_view.rs +++ b/examples/interactive_authentication/web_view.rs @@ -2,6 +2,8 @@ use graph_rs_sdk::oauth::{ web::Theme, web::WebViewOptions, AuthorizationCodeCredential, TokenCredentialExecutor, }; use graph_rs_sdk::Graph; +use std::ops::Add; +use std::time::{Duration, Instant}; static CLIENT_ID: &str = "CLIENT_ID"; static CLIENT_SECRET: &str = "CLIENT_SECRET"; @@ -58,18 +60,3 @@ async fn authenticate() { let body: serde_json::Value = response.json().await.unwrap(); println!("{body:#?}"); } - -async fn customize_webview() { - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .interactive_authentication(Some( - WebViewOptions::builder() - .with_window_title("Sign In") - .with_theme(Theme::Dark) - .with_close_window_on_invalid_navigation(true), - )) - .unwrap(); -} diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index 7da17845..ad3fb77e 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - GenPkce, ProofKeyCodeExchange, ResponseType, Token, TokenCredentialExecutor, TokenRequest, + GenPkce, ProofKeyCodeExchange, ResponseType, Token, TokenCredentialExecutor, }; use lazy_static::lazy_static; use url::Url; diff --git a/examples/oauth/getting_tokens_manually.rs b/examples/oauth/getting_tokens_manually.rs new file mode 100644 index 00000000..f4a70fc3 --- /dev/null +++ b/examples/oauth/getting_tokens_manually.rs @@ -0,0 +1,38 @@ +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, +}; + +// Authorization Code Grant +async fn auth_code_grant( + authorization_code: &str, + client_id: &str, + client_secret: &str, + scope: Vec<String>, + redirect_uri: &str, +) { + let mut confidential_client = + AuthorizationCodeCredential::builder(client_id, client_secret, authorization_code) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .build(); + + let response = confidential_client.execute_async().await.unwrap(); + println!("{response:#?}"); + + let access_token: Token = response.json().await.unwrap(); + println!("{:#?}", access_token.access_token); +} + +// Client Credentials Grant +async fn client_credentials() { + let mut confidential_client = ConfidentialClientApplication::builder("CLIENT_ID") + .with_client_secret("CLIENT_SECRET") + .build(); + + let response = confidential_client.execute_async().await.unwrap(); + println!("{response:#?}"); + + let access_token: Token = response.json().await.unwrap(); + println!("{:#?}", access_token.access_token); +} diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index f03bcdf7..5ef82072 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -10,7 +10,7 @@ //! azure portal. //! //! Microsoft Identity Platform: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-vs-authorization -#![allow(dead_code, unused, unused_imports)] +#![allow(dead_code, unused, unused_imports, clippy::module_inception)] #[macro_use] extern crate serde; @@ -19,6 +19,7 @@ mod auth_code_grant; mod client_credentials; mod device_code; mod environment_credential; +mod getting_tokens_manually; mod is_access_token_expired; mod openid; @@ -26,60 +27,36 @@ use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, - TokenCredentialExecutor, TokenRequest, + TokenCredentialExecutor, }; +use graph_rs_sdk::Graph; fn main() {} -/* - // Some examples of what you can use for authentication and getting access tokens. There are - // more ways to perform oauth authorization. - - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow - auth_code_grant::start_server_main().await; - auth_code_grant_pkce::start_server_main().await; - - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow - client_credentials_admin_consent::start_server_main().await; - - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code - device_code::device_code(); - - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc - open_id_connect::start_server_main().await; -*/ - -// Quick Examples - // Authorization Code Grant -async fn auth_code_grant(authorization_code: &str) { - let pkce = ProofKeyCodeExchange::oneshot().unwrap(); - - let credential = - AuthorizationCodeCredential::builder("CLIENT_ID", "CLIENT_SECRET", authorization_code) - .with_redirect_uri("http://localhost:8000/redirect") +async fn auth_code_grant( + authorization_code: &str, + client_id: &str, + client_secret: &str, + scope: Vec<String>, + redirect_uri: &str, +) { + let mut confidential_client = + AuthorizationCodeCredential::builder(client_id, client_secret, authorization_code) + .with_scope(scope) + .with_redirect_uri(redirect_uri) .unwrap() - .with_pkce(&pkce) .build(); - let mut confidential_client = credential; - - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); - - let access_token: Token = response.json().await.unwrap(); - println!("{:#?}", access_token.access_token); + let _graph_client = Graph::from(&confidential_client); } // Client Credentials Grant async fn client_credentials() { let mut confidential_client = ConfidentialClientApplication::builder("CLIENT_ID") .with_client_secret("CLIENT_SECRET") + .with_tenant("TENANT_ID") .build(); - let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); - - let access_token: Token = response.json().await.unwrap(); - println!("{:#?}", access_token.access_token); + let _graph_client = Graph::from(&confidential_client); } diff --git a/examples/oauth/openid/openid.rs b/examples/oauth/openid/openid.rs index 41fad7c5..3b3a6c03 100644 --- a/examples/oauth/openid/openid.rs +++ b/examples/oauth/openid/openid.rs @@ -1,42 +1,21 @@ -use graph_rs_sdk::oauth::{ - ConfidentialClientApplication, IdToken, OpenIdAuthorizationUrlParameters, OpenIdCredential, - Prompt, ResponseMode, ResponseType, Token, TokenCredentialExecutor, TokenRequest, -}; -use graph_rs_sdk::{error::IdentityResult, Graph}; -use url::Url; +use graph_rs_sdk::oauth::{ConfidentialClientApplication, IdToken}; +use graph_rs_sdk::Graph; -// The client id and client secret must be changed before running this example. -static CLIENT_ID: &str = ""; -static CLIENT_SECRET: &str = ""; -static TENANT_ID: &str = ""; - -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; - -// Use the form post response mode when listening on a server instead -// of the URL query because the the query does not get sent to servers. -fn openid_authorization_url() -> IdentityResult<Url> { - Ok(OpenIdCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT_ID) - //.with_default_scope()? - .with_redirect_uri(REDIRECT_URI)? - .with_response_mode(ResponseMode::FormPost) - .with_response_type([ResponseType::IdToken, ResponseType::Code]) - .with_prompt(Prompt::SelectAccount) - .with_state("1234") - .with_scope(vec!["User.Read", "User.ReadWrite"]) - .build() - .url()?) -} - -// OpenIdCredential will automatically include the openid scope and therefore -// does not need to be added using with_scope -fn get_graph_client(mut id_token: IdToken) -> Graph { - let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_openid(id_token.code.unwrap(), CLIENT_SECRET) - .with_tenant(TENANT_ID) - .with_redirect_uri(REDIRECT_URI) +// OpenIdCredential will automatically include the openid scope +fn get_graph_client( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str>, + id_token: IdToken, +) -> Graph { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_openid(id_token.code.unwrap(), client_secret) + .with_tenant(tenant_id) + .with_redirect_uri(redirect_uri) .unwrap() - .with_scope(vec!["User.Read", "User.ReadWrite"]) + .with_scope(scope) .build(); Graph::from(&confidential_client) diff --git a/examples/oauth_authorization_url/main.rs b/examples/oauth_authorization_url/main.rs index 0e6b7689..ebf4f53e 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/oauth_authorization_url/main.rs @@ -17,7 +17,7 @@ use graph_rs_sdk::oauth::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, - TokenCredentialExecutor, TokenRequest, + TokenCredentialExecutor, }; fn main() {} diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index 769b9401..6fef6e65 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -19,29 +19,35 @@ use url::Url; // See examples/oauth/openid for a full example. -fn open_id_authorization_url( +// Use the form post response mode when listening on a server instead +// of the URL query because the the query does not get sent to servers. +fn openid_authorization_url3( client_id: &str, tenant: &str, redirect_uri: &str, + state: &str, scope: Vec<&str>, ) -> IdentityResult<Url> { - ConfidentialClientApplication::builder(client_id) - .openid_url_builder() + OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) + //.with_default_scope()? .with_redirect_uri(redirect_uri)? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state(state) .with_scope(scope) .build() .url() } - -// Same as above -fn open_id_authorization_url2( +fn open_id_authorization_url( client_id: &str, tenant: &str, redirect_uri: &str, scope: Vec<&str>, ) -> IdentityResult<Url> { - OpenIdCredential::authorization_url_builder(client_id) + ConfidentialClientApplication::builder(client_id) + .openid_url_builder() .with_tenant(tenant) .with_redirect_uri(redirect_uri)? .with_scope(scope) @@ -49,24 +55,17 @@ fn open_id_authorization_url2( .url() } -// Use the form post response mode when listening on a server instead -// of the URL query because the the query does not get sent to servers. -fn openid_authorization_url3( +// Same as above +fn open_id_authorization_url2( client_id: &str, tenant: &str, redirect_uri: &str, - state: &str, scope: Vec<&str>, ) -> IdentityResult<Url> { - Ok(OpenIdCredential::authorization_url_builder(client_id) + OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) - //.with_default_scope()? .with_redirect_uri(redirect_uri)? - .with_response_mode(ResponseMode::FormPost) - .with_response_type([ResponseType::IdToken, ResponseType::Code]) - .with_prompt(Prompt::SelectAccount) - .with_state(state) .with_scope(scope) .build() - .url()?) + .url() } diff --git a/graph-core/src/cache/in_memory_cache_store.rs b/graph-core/src/cache/in_memory_cache_store.rs index a4a90bbc..e524f726 100644 --- a/graph-core/src/cache/in_memory_cache_store.rs +++ b/graph-core/src/cache/in_memory_cache_store.rs @@ -25,4 +25,11 @@ impl<Value: Clone> InMemoryCacheStore<Value> { drop(read_lock); token } + + pub fn evict(&self, cache_id: &str) -> Option<Value> { + let mut write_lock = self.store.write().unwrap(); + let token = write_lock.remove(cache_id); + drop(write_lock); + token + } } diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 9fd1f923..0bdb7495 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -194,7 +194,7 @@ impl GraphClientConfiguration { } } else { Client { - client_application: Box::new(String::default()), + client_application: Box::<String>::default(), inner: builder.build().unwrap(), headers, builder: config, @@ -228,7 +228,7 @@ impl GraphClientConfiguration { } } else { BlockingClient { - client_application: Box::new(String::default()), + client_application: Box::<String>::default(), inner: builder.build().unwrap(), headers, } diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 7cd8e479..2534f233 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -6,7 +6,7 @@ use std::fmt; use url::form_urlencoded::Serializer; use url::Url; -use graph_error::{AuthorizationFailure, GraphFailure, GraphResult, IdentityResult, AF}; +use graph_error::{AuthorizationFailure, GraphResult, IdentityResult, AF}; use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; use crate::oauth::ResponseType; diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index bdcb711b..342e22ab 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -75,6 +75,7 @@ impl AzureCloudInstance { )) } + /* pub fn default_microsoft_graph_scope(&self) -> &'static str { "https://graph.microsoft.com/.default" } @@ -89,6 +90,7 @@ impl AzureCloudInstance { } } } + */ } /// Specifies which Microsoft accounts can be used for sign-in with a given application. diff --git a/graph-oauth/src/identity/authorization_request.rs b/graph-oauth/src/identity/authorization_request_parts.rs similarity index 100% rename from graph-oauth/src/identity/authorization_request.rs rename to graph-oauth/src/identity/authorization_request_parts.rs diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 0bed69f4..a6e0909c 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -2,12 +2,13 @@ use base64::Engine; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; +use graph_error::AF; use reqwest::header::HeaderMap; use url::Url; use uuid::Uuid; use crate::identity::{Authority, AzureCloudInstance}; -use crate::oauth::ForceTokenRefresh; +use crate::oauth::{ApplicationOptions, ForceTokenRefresh}; #[derive(Clone, Default, PartialEq)] pub struct AppConfig { @@ -39,6 +40,30 @@ pub struct AppConfig { pub(crate) log_pii: bool, } +impl TryFrom<ApplicationOptions> for AppConfig { + type Error = AF; + + fn try_from(value: ApplicationOptions) -> Result<Self, Self::Error> { + let client_id = Uuid::try_parse(&value.client_id.to_string()).unwrap_or_default(); + let cache_id = AppConfig::generate_cache_id(client_id, value.tenant_id.as_ref()); + Ok(AppConfig { + tenant_id: value.tenant_id, + client_id: Uuid::try_parse(&value.client_id.to_string())?, + authority: value + .aad_authority_audience + .map(Authority::from) + .unwrap_or_default(), + azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), + extra_query_parameters: Default::default(), + extra_header_parameters: Default::default(), + redirect_uri: None, + cache_id, + force_token_refresh: Default::default(), + log_pii: false, + }) + } +} + impl Debug for AppConfig { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.log_pii { @@ -65,46 +90,28 @@ impl Debug for AppConfig { } impl AppConfig { - pub(crate) fn new() -> AppConfig { - let client_id = Uuid::default(); - let cache_id = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()); - - AppConfig { - tenant_id: None, - client_id, - authority: Default::default(), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: None, - cache_id, - force_token_refresh: Default::default(), - log_pii: Default::default(), + fn generate_cache_id(client_id: Uuid, tenant_id: Option<&String>) -> String { + if let Some(tenant_id) = tenant_id.as_ref() { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( + "{},{}", + tenant_id, + client_id.to_string() + )) + } else { + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()) } } - pub(crate) fn new_init( client_id: Uuid, tenant: Option<impl AsRef<str>>, redirect_uri: Option<Url>, ) -> AppConfig { let tenant_id: Option<String> = tenant.map(|value| value.as_ref().to_string()); - let cache_id = { - if let Some(tenant_id) = tenant_id.as_ref() { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - client_id.to_string() - )) - } else { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()) - } - }; + let cache_id = AppConfig::generate_cache_id(client_id, tenant_id.as_ref()); let authority = tenant_id .clone() - .map(|tenant| Authority::TenantId(tenant)) + .map(Authority::TenantId) .unwrap_or_default(); AppConfig { @@ -123,8 +130,7 @@ impl AppConfig { pub(crate) fn new_with_client_id(client_id: impl AsRef<str>) -> AppConfig { let client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); - let cache_id = - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()); + let cache_id = AppConfig::generate_cache_id(client_id, None); AppConfig { tenant_id: None, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 9a486365..cc7006a0 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -3,8 +3,7 @@ use std::fmt::{Debug, Formatter}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; -use time::{Duration, Instant}; -use url::form_urlencoded::Serializer; + use url::Url; use uuid::Uuid; @@ -14,8 +13,8 @@ use graph_error::{IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, - ConfidentialClientApplication, Prompt, ResponseMode, ResponseType, + AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, + ResponseType, }; //#[cfg(feature = "interactive-auth")] @@ -202,36 +201,34 @@ impl AuthCodeAuthorizationUrlParameters { return match next { None => Err(anyhow::anyhow!("Unknown")), - Some(auth_event) => { - match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) - } - InteractiveAuthEvent::TimedOut(duration) => { - Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; - - let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; - Ok(response_query) - } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => { - Err(anyhow::anyhow!("CloseRequested")) - } - WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) - } + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = + serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) } + WindowCloseReason::TimedOut { + start: _, + requested_resume: _, + } => Err(anyhow::anyhow!("TimedOut")), } } - } + }, }; } } @@ -249,10 +246,7 @@ pub(crate) mod web_view_authenticator { &self, interactive_web_view_options: Option<WebViewOptions>, ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { - let uri = self - .app_config - .azure_cloud_instance - .auth_uri(&self.app_config.authority)?; + let uri = self.url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let _timeout = web_view_options.timeout; diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index e67e0761..cc2d0b11 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -4,11 +4,11 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; -use url::Url; + use uuid::Uuid; use graph_core::cache::{InMemoryCacheStore, TokenCache}; -use graph_error::{AuthExecutionError, IdentityResult, AF}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -23,6 +23,8 @@ credential_builder!( ConfidentialClientApplication<AuthorizationCodeAssertionCredential> ); +/// Authorization Code Using An Assertion +/// /// The OAuth 2.0 authorization code grant type, or auth code flow, enables a client application /// to obtain authorized access to protected resources like web APIs. The auth code flow requires /// a user-agent that supports redirection from the authorization server (the Microsoft @@ -99,10 +101,15 @@ impl AuthorizationCodeAssertionCredential { } pub fn builder( + client_id: impl AsRef<str>, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( - Default::default(), + AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + None, + ), authorization_code, ) } @@ -112,6 +119,33 @@ impl AuthorizationCodeAssertionCredential { ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } #[async_trait] @@ -120,57 +154,84 @@ impl TokenCache for AuthorizationCodeAssertionCredential { fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) - } else { - Ok(token) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { + return Ok(token); + } + } + + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh(cache_id) + } else { + Ok(token) + } + } else { + self.execute_cached_token_refresh(cache_id) + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh(cache_id); + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) - } else { - Ok(token.clone()) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self + .execute_cached_token_refresh_async(cache_id.clone()) + .await + { + return Ok(token); + } + } + + if let Some(old_token) = self.token_cache.get(cache_id.as_str()) { + if old_token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = old_token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh_async(cache_id).await + } else { + Ok(old_token.clone()) + } + } else { + self.execute_cached_token_refresh_async(cache_id).await + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh_async(cache_id).await; + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } } #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("token_url"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index a1b7cdf1..200f0046 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -4,18 +4,18 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; -use url::Url; + use uuid::Uuid; use graph_core::cache::{InMemoryCacheStore, TokenCache}; -use graph_error::{AuthExecutionError, IdentityResult, AF}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters, Authority, - AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, - TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, + AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, + ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, + CLIENT_ASSERTION_TYPE, }; #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; @@ -118,6 +118,33 @@ impl AuthorizationCodeCertificateCredential { ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } #[async_trait] @@ -126,56 +153,84 @@ impl TokenCache for AuthorizationCodeCertificateCredential { fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) - } else { - Ok(token) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { + return Ok(token); + } + } + + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh(cache_id) + } else { + Ok(token) + } + } else { + self.execute_cached_token_refresh(cache_id) + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh(cache_id); + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) - } else { - Ok(token.clone()) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self + .execute_cached_token_refresh_async(cache_id.clone()) + .await + { + return Ok(token); + } + } + + if let Some(old_token) = self.token_cache.get(cache_id.as_str()) { + if old_token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = old_token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh_async(cache_id).await + } else { + Ok(old_token.clone()) + } + } else { + self.execute_cached_token_refresh_async(cache_id).await + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh_async(cache_id).await; + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } } + #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("token_url"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index feae6f14..2e1651c5 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -135,9 +135,8 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - match self.execute_cached_token_refresh(cache_id.clone()) { - Ok(token) => return Ok(token), - Err(_) => {} + if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { + return Ok(token); } } @@ -149,7 +148,7 @@ impl TokenCache for AuthorizationCodeCredential { self.execute_cached_token_refresh(cache_id) } else { - Ok(token.clone()) + Ok(token) } } else { self.execute_cached_token_refresh(cache_id) @@ -173,12 +172,11 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - match self + if let Ok(token) = self .execute_cached_token_refresh_async(cache_id.clone()) .await { - Ok(token) => return Ok(token), - Err(_) => {} + return Ok(token); } } @@ -364,18 +362,6 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { @@ -565,8 +551,8 @@ mod test { fn should_force_refresh_test() { let uuid_value = Uuid::new_v4().to_string(); let mut credential_builder = - AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); - let mut credential = credential_builder + AuthorizationCodeCredential::builder(uuid_value, "secret".to_string(), "code"); + let _credential = credential_builder .with_redirect_uri("https://localhost") .unwrap() .with_client_secret("client_secret") diff --git a/graph-oauth/src/identity/credentials/bearer_token_credential.rs b/graph-oauth/src/identity/credentials/bearer_token_credential.rs index bcc6e7cf..08fd193f 100644 --- a/graph-oauth/src/identity/credentials/bearer_token_credential.rs +++ b/graph-oauth/src/identity/credentials/bearer_token_credential.rs @@ -2,7 +2,6 @@ use async_trait::async_trait; use graph_core::cache::AsBearer; use graph_core::identity::ClientApplication; use graph_error::AuthExecutionResult; -use std::borrow::Cow; #[derive(Clone)] pub struct BearerTokenCredential(String); diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index b1fd4154..abc340fa 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; -use url::Url; + use uuid::Uuid; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -21,17 +21,44 @@ credential_builder!( ConfidentialClientApplication<ClientAssertionCredential> ); +/// Client Credentials Using an Assertion. +/// +/// The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use +/// its own credentials, instead of impersonating a user, to authenticate when calling another +/// web service. +/// +/// Everything in the request is the same as the certificate-based flow, with the crucial exception +/// of the source of the client_assertion. In this flow, your application does not create the JWT +/// assertion itself. Instead, your app uses a JWT created by another identity provider. +/// This is called workload identity federation, where your apps identity in another identity +/// platform is used to acquire tokens inside the Microsoft identity platform. This is best +/// suited for cross-cloud scenarios, such as hosting your compute outside Azure but accessing +/// APIs protected by Microsoft identity platform. For information about the required format +/// of JWTs created by other identity providers, read about the assertion format. +/// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#third-case-access-token-request-with-a-federated-credential #[derive(Clone)] pub struct ClientAssertionCredential { pub(crate) app_config: AppConfig, - /// The value passed for the scope parameter in this request should be the resource - /// identifier (application ID URI) of the resource you want, affixed with the .default - /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// Default is https://graph.microsoft.com/.default. + /// The value passed for the scope parameter in this request should be the resource identifier + /// (application ID URI) of the resource you want, affixed with the .default suffix. + /// All scopes included must be for a single resource. Including scopes for multiple + /// resources will result in an error. + /// + /// For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// This value tells the Microsoft identity platform that of all the direct application + /// permissions you have configured for your app, the endpoint should issue a token for the + /// ones associated with the resource you want to use. To learn more about the /.default scope, + /// see the [consent documentation](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#the-default-scope). pub(crate) scope: Vec<String>, + /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + /// This is automatically set by the SDK. pub(crate) client_assertion_type: String, + /// An assertion (a JWT, or JSON web token) that your application gets from another identity + /// provider outside of Microsoft identity platform, like Kubernetes. The specifics of this + /// JWT must be registered on your application as a federated identity credential. Read about + /// workload identity federation to learn how to setup and use assertions generated from + /// other identity providers. pub(crate) client_assertion: String, - pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -47,7 +74,6 @@ impl ClientAssertionCredential { scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: assertion.as_ref().to_string(), - refresh_token: None, serializer: Default::default(), token_cache: Default::default(), } @@ -112,15 +138,20 @@ pub struct ClientAssertionCredentialBuilder { } impl ClientAssertionCredentialBuilder { - #[allow(dead_code)] - pub(crate) fn new() -> ClientAssertionCredentialBuilder { + pub fn new( + client_id: impl AsRef<str>, + signed_assertion: impl AsRef<str>, + ) -> ClientAssertionCredentialBuilder { ClientAssertionCredentialBuilder { credential: ClientAssertionCredential { - app_config: Default::default(), + app_config: AppConfig::new_init( + Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), + Option::<String>::None, + None, + ), scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), - client_assertion: Default::default(), - refresh_token: None, + client_assertion: signed_assertion.as_ref().to_owned(), serializer: Default::default(), token_cache: Default::default(), }, @@ -137,7 +168,6 @@ impl ClientAssertionCredentialBuilder { scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), client_assertion: signed_assertion, - refresh_token: None, serializer: Default::default(), token_cache: Default::default(), }, @@ -148,27 +178,10 @@ impl ClientAssertionCredentialBuilder { self.credential.client_assertion = client_assertion.as_ref().to_owned(); self } - - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); - self - } } #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_err("token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.client_id().to_string(); if client_id.trim().is_empty() { @@ -193,41 +206,17 @@ impl TokenCredentialExecutor for ClientAssertionCredential { .add_scope("https://graph.microsoft.com/.default"); } - return if let Some(refresh_token) = self.refresh_token.as_ref() { - if refresh_token.trim().is_empty() { - return AF::msg_result( - OAuthParameter::RefreshToken.alias(), - "refresh_token is set but is empty", - ); - } - - self.serializer - .refresh_token(refresh_token.as_ref()) - .grant_type("refresh_token"); - - self.serializer.as_credential_map( - vec![OAuthParameter::Scope], - vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, - OAuthParameter::RefreshToken, - ], - ) - } else { - self.serializer.grant_type("client_credentials"); - - self.serializer.as_credential_map( - vec![OAuthParameter::Scope], - vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, - ], - ) - }; + self.serializer.grant_type("client_credentials"); + + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ) } fn client_id(&self) -> &Uuid { diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 3481eaf9..f3a1f511 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -3,11 +3,11 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; -use url::Url; + use uuid::Uuid; use graph_core::cache::{InMemoryCacheStore, TokenCache}; -use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult, AF}; +use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -26,19 +26,48 @@ credential_builder!( ConfidentialClientApplication<ClientCertificateCredential> ); -/// https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-certificate-credentials +/// Client Credentials Using A Certificate +/// +/// The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use +/// its own credentials, instead of impersonating a user, to authenticate when calling another +/// web service. The grant specified in RFC 6749, sometimes called two-legged OAuth, can be used +/// to access web-hosted resources by using the identity of an application. This type is commonly +/// used for server-to-server interactions that must run in the background, without immediate +/// interaction with a user, and is often referred to as daemons or service accounts. +/// For more information on the flow see +/// [Token Request With a Certificate](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) +/// +/// The SDK handles certificates and creating the assertion automatically using the +/// openssl crate. This is significantly easier than having to format the assertion from +/// the certificate yourself. If you need to use your own assertion see +/// [ClientAssertionCredential](crate::identity::ClientAssertionCredential) #[derive(Clone)] #[allow(dead_code)] pub struct ClientCertificateCredential { pub(crate) app_config: AppConfig, - /// The value passed for the scope parameter in this request should be the resource - /// identifier (application ID URI) of the resource you want, affixed with the .default - /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// Default is https://graph.microsoft.com/.default. + /// The value passed for the scope parameter in this request should be the resource identifier + /// (application ID URI) of the resource you want, affixed with the .default suffix. + /// All scopes included must be for a single resource. Including scopes for multiple + /// resources will result in an error. + /// + /// For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// This value tells the Microsoft identity platform that of all the direct application + /// permissions you have configured for your app, the endpoint should issue a token for the + /// ones associated with the resource you want to use. To learn more about the /.default scope, + /// see the [consent documentation](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#the-default-scope). pub(crate) scope: Vec<String>, + /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. + /// This value is automatically set by the SDK. pub(crate) client_assertion_type: String, + /// An assertion (a JSON web token) that you need to create and sign with the certificate + /// you registered as credentials for your application. Read about + /// [certificate credentials](https://learn.microsoft.com/en-us/entra/identity-platform/certificate-credentials) + /// to learn how to register your certificate and the format of the assertion. + /// + /// The SDK handles certificates and creating the assertion automatically using the + /// openssl crate. This is significantly easier than having to format the assertion from + /// the certificate yourself. pub(crate) client_assertion: String, - pub(crate) refresh_token: Option<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -50,7 +79,6 @@ impl ClientCertificateCredential { scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - refresh_token: None, serializer: Default::default(), token_cache: Default::default(), } @@ -66,11 +94,6 @@ impl ClientCertificateCredential { Ok(builder.credential) } - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.refresh_token = Some(refresh_token.as_ref().to_owned()); - self - } - pub fn builder<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder::new(client_id) } @@ -136,18 +159,6 @@ impl TokenCache for ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_err("token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { @@ -165,48 +176,23 @@ impl TokenCredentialExecutor for ClientCertificateCredential { self.serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) - .client_assertion_type(self.client_assertion_type.as_str()); + .client_assertion_type(self.client_assertion_type.as_str()) + .grant_type("client_credentials"); if self.scope.is_empty() { self.serializer .add_scope("https://graph.microsoft.com/.default"); } - return if let Some(refresh_token) = self.refresh_token.as_ref() { - if refresh_token.trim().is_empty() { - return AuthorizationFailure::msg_result( - OAuthParameter::RefreshToken.alias(), - "refresh_token is set but is empty", - ); - } - - self.serializer - .refresh_token(refresh_token.as_ref()) - .grant_type("refresh_token"); - - self.serializer.as_credential_map( - vec![OAuthParameter::Scope], - vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, - OAuthParameter::RefreshToken, - ], - ) - } else { - self.serializer.grant_type("client_credentials"); - - self.serializer.as_credential_map( - vec![OAuthParameter::Scope], - vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, - ], - ) - }; + self.serializer.as_credential_map( + vec![OAuthParameter::Scope], + vec![ + OAuthParameter::ClientId, + OAuthParameter::GrantType, + OAuthParameter::ClientAssertion, + OAuthParameter::ClientAssertionType, + ], + ) } fn client_id(&self) -> &Uuid { @@ -238,8 +224,7 @@ impl ClientCertificateCredentialBuilder { app_config: AppConfig::new_with_client_id(client_id.as_ref()), scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: String::new(), - refresh_token: None, + client_assertion: Default::default(), serializer: OAuthSerializer::new(), token_cache: Default::default(), }, @@ -256,8 +241,7 @@ impl ClientCertificateCredentialBuilder { app_config, scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: String::new(), - refresh_token: None, + client_assertion: Default::default(), serializer: OAuthSerializer::new(), token_cache: Default::default(), }, @@ -281,11 +265,6 @@ impl ClientCertificateCredentialBuilder { self } - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); - self - } - pub fn credential(self) -> ClientCertificateCredential { self.credential } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index fc751400..42e546df 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -1,5 +1,5 @@ use reqwest::IntoUrl; -use url::form_urlencoded::Serializer; + use url::Url; use uuid::Uuid; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 2494a48a..ced21e58 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; -use url::Url; + use uuid::Uuid; use graph_core::cache::{InMemoryCacheStore, TokenCache}; @@ -143,21 +143,6 @@ impl TokenCache for ClientSecretCredential { #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.authority()); - - let uri = - self.serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AuthorizationFailure::msg_err( - "token_url for access and refresh tokens missing", - "Internal Error", - ))?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 1801f01e..47d96d1d 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use graph_core::cache::{AsBearer, TokenCache}; use graph_core::identity::ClientApplication; -use graph_error::{AuthExecutionResult, GraphResult, IdentityResult, AF}; +use graph_error::{AuthExecutionResult, IdentityResult, AF}; use crate::identity::{ credentials::app_config::AppConfig, @@ -19,7 +19,7 @@ use crate::identity::{ AuthorizationCodeCredential, AuthorizationQueryResponse, AzureCloudInstance, ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, }; -use crate::oauth::{AuthCodeAuthorizationUrlParameterBuilder, AuthCodeAuthorizationUrlParameters}; +use crate::oauth::AuthCodeAuthorizationUrlParameters; use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, }; @@ -211,36 +211,34 @@ impl ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { return match next { None => Err(anyhow::anyhow!("Unknown")), - Some(auth_event) => { - match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) - } - InteractiveAuthEvent::TimedOut(duration) => { - Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; - - let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; - Ok(response_query) - } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => { - Err(anyhow::anyhow!("CloseRequested")) - } - WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) - } + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = + serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) } + WindowCloseReason::TimedOut { + start: _, + requested_resume: _, + } => Err(anyhow::anyhow!("TimedOut")), } } - } + }, }; } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 54686e88..6b07d9a4 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -25,8 +25,8 @@ use crate::identity::{ PollDeviceCodeType, PublicClientApplication, TokenCredentialExecutor, }; use crate::oauth::Token; +use crate::web::InteractiveAuthenticator; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; -use crate::web::{InteractiveAuthenticator, InteractiveWebView}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -91,6 +91,33 @@ impl DeviceCodeCredential { pub fn builder(client_id: impl AsRef<str>) -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder::new(client_id.as_ref()) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + let new_token: Token = response.json().await?; + + if new_token.refresh_token.is_some() { + self.refresh_token = new_token.refresh_token.clone(); + } + + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } impl Debug for DeviceCodeCredential { @@ -108,39 +135,78 @@ impl TokenCache for DeviceCodeCredential { fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let new_token: Token = response.json()?; - self.token_cache.store(cache_id, new_token.clone()); - Ok(new_token) - } else { - Ok(token) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { + return Ok(token); + } + } + + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh(cache_id) + } else { + Ok(token) + } + } else { + self.execute_cached_token_refresh(cache_id) + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh(cache_id); + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute()?; - let new_token: Token = response.json()?; - self.token_cache.store(cache_id, new_token.clone()); - Ok(new_token) } } async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); - if let Some(token) = self.token_cache.get(cache_id.as_str()) { - if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let new_token: Token = response.json().await?; - self.token_cache.store(cache_id, new_token.clone()); - Ok(new_token) - } else { - Ok(token.clone()) + + match self.app_config.force_token_refresh { + ForceTokenRefresh::Never => { + // Attempt to bypass a read on the token store by using previous + // refresh token stored outside of RwLock + if self.refresh_token.is_some() { + if let Ok(token) = self + .execute_cached_token_refresh_async(cache_id.clone()) + .await + { + return Ok(token); + } + } + + if let Some(old_token) = self.token_cache.get(cache_id.as_str()) { + if old_token.is_expired_sub(time::Duration::minutes(5)) { + if let Some(refresh_token) = old_token.refresh_token.as_ref() { + self.refresh_token = Some(refresh_token.to_owned()); + } + + self.execute_cached_token_refresh_async(cache_id).await + } else { + Ok(old_token.clone()) + } + } else { + self.execute_cached_token_refresh_async(cache_id).await + } + } + ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + let token_result = self.execute_cached_token_refresh_async(cache_id).await; + if self.app_config.force_token_refresh == ForceTokenRefresh::Once { + self.app_config.force_token_refresh = ForceTokenRefresh::Never; + } + token_result } - } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) } } } @@ -152,17 +218,9 @@ impl TokenCredentialExecutor for DeviceCodeCredential { .authority_device_code(&azure_cloud_instance, &self.authority()); if self.device_code.is_none() && self.refresh_token.is_none() { - let uri = self - .serializer - .get(OAuthParameter::AuthorizationUrl) - .ok_or(AF::msg_internal_err("authorization_url"))?; - Url::parse(uri.as_str()).map_err(|_err| AF::msg_internal_err("authorization_url")) + Ok(self.azure_cloud_instance().auth_uri(&self.authority())?) } else { - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("token_url"))?; - Url::parse(uri.as_str()).map_err(|_err| AF::msg_internal_err("token_url")) + Ok(self.azure_cloud_instance().token_uri(&self.authority())?) } } @@ -289,14 +347,9 @@ impl DeviceCodeCredentialBuilder { self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); self } - - /* - pub fn build(&self) -> PublicClientApplication { - PublicClientApplication::from(self.credential.clone()) - } - */ } +/* impl From<&DeviceCode> for DeviceCodeCredentialBuilder { fn from(value: &DeviceCode) -> Self { DeviceCodeCredentialBuilder { @@ -311,6 +364,7 @@ impl From<&DeviceCode> for DeviceCodeCredentialBuilder { } } } + */ pub struct DeviceCodePollingExecutor { credential: DeviceCodeCredential, @@ -349,36 +403,34 @@ impl DeviceCodePollingExecutor { return match next { None => Err(anyhow::anyhow!("Unknown")), - Some(auth_event) => { - match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) - } - InteractiveAuthEvent::TimedOut(duration) => { - Err(anyhow::anyhow!("Webview timed out while waiting on redirect to valid redirect uri with timeout duration of {duration:#?}")) - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; - - let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; - Ok(response_query) - } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => { - Err(anyhow::anyhow!("CloseRequested")) - } - WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) - } + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let url_str = uri.as_str(); + let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( + "query | fragment", + &format!("No query or fragment returned on redirect uri: {url_str}"), + ))?; + + let response_query: AuthorizationQueryResponse = + serde_urlencoded::from_str(query)?; + Ok(response_query) + } + InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + match window_close_reason { + WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), + WindowCloseReason::InvalidWindowNavigation => { + Err(anyhow::anyhow!("InvalidWindowNavigation")) } + WindowCloseReason::TimedOut { + start: _, + requested_resume: _, + } => Err(anyhow::anyhow!("TimedOut")), } } - } + }, }; } diff --git a/graph-oauth/src/identity/credentials/environment_credential.rs b/graph-oauth/src/identity/credentials/environment_credential.rs index c78a9c7f..0b931c4e 100644 --- a/graph-oauth/src/identity/credentials/environment_credential.rs +++ b/graph-oauth/src/identity/credentials/environment_credential.rs @@ -1,18 +1,9 @@ -use std::collections::HashMap; use std::env::VarError; use std::fmt::{Debug, Formatter}; -use url::Url; -use uuid::Uuid; - -use graph_error::IdentityResult; - -use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ClientSecretCredential, TokenCredentialExecutor, -}; -use crate::oauth::{ - ConfidentialClientApplication, PublicClientApplication, ResourceOwnerPasswordCredential, + ClientSecretCredential, ConfidentialClientApplication, PublicClientApplication, + ResourceOwnerPasswordCredential, }; const AZURE_TENANT_ID: &str = "AZURE_TENANT_ID"; @@ -22,9 +13,7 @@ const AZURE_USERNAME: &str = "AZURE_USERNAME"; const AZURE_PASSWORD: &str = "AZURE_PASSWORD"; #[derive(Clone)] -pub struct EnvironmentCredential { - pub credential: Box<dyn TokenCredentialExecutor + Send>, -} +pub struct EnvironmentCredential; impl Debug for EnvironmentCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { @@ -139,64 +128,3 @@ impl EnvironmentCredential { } } } - -impl TokenCredentialExecutor for EnvironmentCredential { - fn uri(&mut self) -> IdentityResult<Url> { - self.credential.uri() - } - - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - self.credential.form_urlencode() - } - - fn client_id(&self) -> &Uuid { - self.credential.client_id() - } - - fn authority(&self) -> Authority { - self.credential.authority() - } - - fn azure_cloud_instance(&self) -> AzureCloudInstance { - self.app_config().azure_cloud_instance - } - - fn app_config(&self) -> &AppConfig { - self.credential.app_config() - } -} - -/* -impl From<ClientSecretCredential> for EnvironmentCredential { - fn from(value: ClientSecretCredential) -> Self { - EnvironmentCredential { - credential: Box::new(value), - } - } -} - -impl From<ResourceOwnerPasswordCredential> for EnvironmentCredential { - fn from(value: ResourceOwnerPasswordCredential) -> Self { - EnvironmentCredential { - credential: Box::new(value), - } - } -} - -impl From<ConfidentialClientApplication> for EnvironmentCredential { - fn from(value: ConfidentialClientApplication) -> Self { - EnvironmentCredential { - credential: Box::new(value), - } - } -} - -impl From<PublicClientApplication> for EnvironmentCredential { - fn from(value: PublicClientApplication) -> Self { - EnvironmentCredential { - credential: Box::new(value), - } - } -} - - */ diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs index 5caae5cb..afedb2b7 100644 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs @@ -1,4 +1,3 @@ -use url::form_urlencoded::Serializer; use url::Url; use graph_error::{AuthorizationFailure, IdentityResult}; diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 105b2eed..b9d9ae30 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -3,13 +3,13 @@ use graph_error::{AuthorizationFailure, IdentityResult}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; -use url::form_urlencoded::Serializer; + use url::Url; use uuid::*; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{AzureCloudInstance, ForceTokenRefresh, Prompt, ResponseMode, ResponseType}; +use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; credential_builder_base!(ImplicitCredentialBuilder); diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs index daaefbfe..3137a520 100644 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs @@ -1,4 +1,3 @@ -use url::form_urlencoded::Serializer; use url::Url; use graph_error::{AuthorizationFailure, IdentityResult, AF}; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 6f54d9ed..8d96a246 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use std::fmt::{Debug, Formatter}; use reqwest::IntoUrl; -use url::form_urlencoded::Serializer; + use url::Url; use uuid::Uuid; diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 2fada234..9a0f8629 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -107,8 +107,8 @@ impl OpenIdCredential { self.refresh_token = Some(refresh_token.as_ref().to_owned()); } - pub fn builder() -> OpenIdCredentialBuilder { - OpenIdCredentialBuilder::new() + pub fn builder(client_id: impl TryInto<Uuid>) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new(client_id) } pub fn authorization_url_builder( @@ -163,9 +163,8 @@ impl TokenCache for OpenIdCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - match self.execute_cached_token_refresh(cache_id.clone()) { - Ok(token) => return Ok(token), - Err(_) => {} + if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { + return Ok(token); } } @@ -177,7 +176,7 @@ impl TokenCache for OpenIdCredential { self.execute_cached_token_refresh(cache_id) } else { - Ok(token.clone()) + Ok(token) } } else { self.execute_cached_token_refresh(cache_id) @@ -201,12 +200,11 @@ impl TokenCache for OpenIdCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - match self + if let Ok(token) = self .execute_cached_token_refresh_async(cache_id.clone()) .await { - Ok(token) => return Ok(token), - Err(_) => {} + return Ok(token); } } @@ -237,18 +235,6 @@ impl TokenCache for OpenIdCredential { #[async_trait] impl TokenCredentialExecutor for OpenIdCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.app_config.authority); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_internal_err("access_token_url"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { @@ -354,10 +340,27 @@ pub struct OpenIdCredentialBuilder { } impl OpenIdCredentialBuilder { - fn new() -> OpenIdCredentialBuilder { - let redirect_url = Url::parse("http://localhost").expect("Internal Error - please report"); - let mut app_config = AppConfig::new(); - app_config.redirect_uri = Some(redirect_url); + fn new(client_id: impl TryInto<Uuid>) -> OpenIdCredentialBuilder { + Self { + credential: OpenIdCredential { + app_config: AppConfig::new_init( + client_id.try_into().unwrap_or_default(), + Option::<String>::None, + Some(Url::parse("http://localhost").expect("Internal Error - please report")), + ), + authorization_code: None, + refresh_token: None, + client_secret: String::new(), + scope: vec!["openid".to_owned()], + code_verifier: None, + pkce: None, + serializer: Default::default(), + token_cache: Default::default(), + }, + } + } + + fn new_with_app_config(app_config: AppConfig) -> OpenIdCredentialBuilder { Self { credential: OpenIdCredential { app_config, @@ -441,8 +444,7 @@ impl OpenIdCredentialBuilder { impl From<OpenIdAuthorizationUrlParameters> for OpenIdCredentialBuilder { fn from(value: OpenIdAuthorizationUrlParameters) -> Self { - let mut builder = OpenIdCredentialBuilder::new(); - builder.credential.app_config = value.app_config; + let mut builder = OpenIdCredentialBuilder::new_with_app_config(value.app_config); builder.with_scope(value.scope); builder @@ -461,7 +463,7 @@ mod test { #[test] fn with_tenant_id_common() { - let credential = OpenIdCredential::builder() + let credential = OpenIdCredential::builder(Uuid::new_v4()) .with_authority(Authority::TenantId("common".into())) .build(); @@ -470,7 +472,7 @@ mod test { #[test] fn with_tenant_id_adfs() { - let credential = OpenIdCredential::builder() + let credential = OpenIdCredential::builder(Uuid::new_v4()) .with_authority(Authority::AzureDirectoryFederatedServices) .build(); diff --git a/graph-oauth/src/identity/credentials/prompt.rs b/graph-oauth/src/identity/credentials/prompt.rs index 90967ff1..4b4750a8 100644 --- a/graph-oauth/src/identity/credentials/prompt.rs +++ b/graph-oauth/src/identity/credentials/prompt.rs @@ -16,15 +16,12 @@ use crate::identity::credentials::as_query::AsQuery; pub enum Prompt { #[default] None, - /// The user will be prompted for credentials by the service. It is achieved - /// by sending <prompt=login to the Azure AD service. + /// The user will be prompted for credentials by the service. Login, - /// The user will be prompted to consent even if consent was granted before. It is achieved - /// by sending prompt=consent to Azure AD. + /// The user will be prompted to consent even if consent was granted before. Consent, - /// AcquireToken will send prompt=select_account to Azure AD's authorize endpoint - /// which would present to the user a list of accounts from which one can be selected for - /// authentication. + /// The user will be prompted with a list of accounts from which one can be selected + /// for authentication. SelectAccount, /// Use only for federated users. Provides same functionality as prompt=none /// for managed users. diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index b754434d..d65210ea 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,15 +1,11 @@ -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; - -use async_trait::async_trait; -use url::Url; -use uuid::Uuid; - -use graph_error::{IdentityResult, AF}; - use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; +use async_trait::async_trait; +use graph_error::{IdentityResult, AF}; +use std::collections::HashMap; +use std::fmt::{Debug, Formatter}; +use uuid::Uuid; /// Allows an application to sign in the user by directly handling their password. /// Not recommended. ROPC can also be done using a client secret or assertion, @@ -83,18 +79,6 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { - fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority(&azure_cloud_instance, &self.app_config.authority); - - let uri = self - .serializer - .get(OAuthParameter::TokenUrl) - .ok_or(AF::msg_err("access_token_url", "Internal Error"))?; - Url::parse(uri.as_str()).map_err(AF::from) - } - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 46f945b6..6ec027b2 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -18,11 +18,13 @@ dyn_clone::clone_trait_object!(TokenCredentialExecutor); #[async_trait] pub trait TokenCredentialExecutor: DynClone + Debug { - fn uri(&mut self) -> IdentityResult<Url>; + fn uri(&mut self) -> IdentityResult<Url> { + Ok(self.azure_cloud_instance().token_uri(&self.authority())?) + } fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; - fn authorization_request_parts(&mut self) -> IdentityResult<AuthorizationRequestParts> { + fn request_parts(&mut self) -> IdentityResult<AuthorizationRequestParts> { let uri = self.uri()?; let form = self.form_urlencode()?; let basic_auth = self.basic_auth(); @@ -43,7 +45,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .https_only(true) .build()?; - let auth_request = self.authorization_request_parts()?; + let auth_request = self.request_parts()?; let basic_auth = auth_request.basic_auth; if let Some((client_identifier, secret)) = basic_auth { @@ -77,7 +79,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .https_only(true) .build()?; - let auth_request = self.authorization_request_parts()?; + let auth_request = self.request_parts()?; let basic_auth = auth_request.basic_auth; if let Some((client_identifier, secret)) = basic_auth { diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index fae00ab9..7f1f5c0e 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -8,7 +8,7 @@ pub use allowed_host_validator::*; pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; -pub use authorization_request::*; +pub use authorization_request_parts::*; pub use authorization_serializer::*; pub use credentials::*; pub use device_code::*; @@ -20,7 +20,7 @@ mod allowed_host_validator; mod application_options; mod authority; mod authorization_query_response; -mod authorization_request; +mod authorization_request_parts; mod authorization_serializer; mod credentials; mod device_code; diff --git a/graph-oauth/src/identity/token_validator.rs b/graph-oauth/src/identity/token_validator.rs index 1c24d21d..41a2dd4f 100644 --- a/graph-oauth/src/identity/token_validator.rs +++ b/graph-oauth/src/identity/token_validator.rs @@ -8,7 +8,8 @@ impl TokenValidator { TokenValidator::default() } - pub fn with_application_id(&mut self, aud: impl AsRef<str>) -> &mut Self { + // Validate the audience + pub fn with_aud(&mut self, aud: impl AsRef<str>) -> &mut Self { self.application_id = Some(aud.as_ref().to_owned()); self } diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index 87caedfd..c96477ff 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -1,5 +1,6 @@ use crate::web::WebViewOptions; use graph_error::IdentityResult; +use std::time::Instant; use url::Url; pub trait InteractiveAuthenticator { @@ -13,12 +14,15 @@ pub trait InteractiveAuthenticator { pub enum WindowCloseReason { CloseRequested, InvalidWindowNavigation, + TimedOut { + start: Instant, + requested_resume: Instant, + }, } #[derive(Clone, Debug)] pub enum InteractiveAuthEvent { InvalidRedirectUri(String), - TimedOut(std::time::Duration), ReachedRedirectUri(Url), ClosingWindow(WindowCloseReason), } diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index de4bf013..aef07e74 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -140,7 +140,7 @@ impl InteractiveWebView { .unwrap_or_default(); // Wait time to avoid deadlock where window closes before // the channel has received the redirect uri. - std::thread::sleep(Duration::from_secs(1)); + let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); return true; } @@ -151,7 +151,7 @@ impl InteractiveWebView { is_valid_host } else { - tracing::trace!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); + tracing::debug!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); let _ = proxy.send_event(UserEvents::CloseWindow); false } @@ -163,17 +163,45 @@ impl InteractiveWebView { match event { Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), + Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { + sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::TimedOut { + start, requested_resume + })).unwrap_or_default(); + tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::CloseRequested)).unwrap_or_default(); tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?}"); - tracing::trace!(target: "interactive_webview", "Closing window"); + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { @@ -182,12 +210,11 @@ impl InteractiveWebView { tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap_or_default(); - // Clear browsing data in the event of invalid navigation as we don't - // know if there is a security issue. - let _ = webview.clear_all_browsing_data(); + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } - // Wait time to avoid deadlock where window closes before - // the channel has received last event. + // Wait time to avoid deadlock where window closes before receiver gets the event std::thread::sleep(Duration::from_secs(1)); *control_flow = ControlFlow::Exit; diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index dc6f7202..e8be0a2b 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -39,7 +39,7 @@ impl WebViewOptions { } pub fn with_ports(mut self, ports: &[usize]) -> Self { - self.ports = ports.into_iter().cloned().collect(); + self.ports = ports.to_vec(); self } @@ -48,7 +48,7 @@ impl WebViewOptions { self } - pub fn with_clear_browsing_data_on_window_close(mut self, clear_browsing_data: bool) -> Self { + pub fn with_clear_browsing_data(mut self, clear_browsing_data: bool) -> Self { self.clear_browsing_data = clear_browsing_data; self } From c760d5a7d05358029ca89fb07185b37be5c2f571 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 3 Nov 2023 21:25:46 -0400 Subject: [PATCH 055/118] Interactive authentication improvements --- examples/client_configuration.rs | 23 +++++- .../customize_webview.rs | 2 +- .../interactive_authentication/web_view.rs | 2 +- .../interactive_authentication.rs | 70 ------------------- examples/oauth/auth_code_grant/mod.rs | 3 - .../client_credentials_secret.rs | 20 ++++++ examples/oauth/client_credentials/mod.rs | 23 +----- .../client_credentials_admin_consent.rs | 0 .../client_credentials/server_examples/mod.rs | 1 + examples/oauth/getting_tokens_manually.rs | 26 +++++-- examples/paging_and_next_links.rs | 2 + .../auth_code_authorization_url.rs | 10 +-- src/lib.rs | 7 -- 13 files changed, 76 insertions(+), 113 deletions(-) delete mode 100644 examples/oauth/auth_code_grant/interactive_authentication.rs create mode 100644 examples/oauth/client_credentials/client_credentials_secret.rs rename examples/oauth/client_credentials/{ => server_examples}/client_credentials_admin_consent.rs (100%) create mode 100644 examples/oauth/client_credentials/server_examples/mod.rs diff --git a/examples/client_configuration.rs b/examples/client_configuration.rs index 851d8be4..8ea7f0dc 100644 --- a/examples/client_configuration.rs +++ b/examples/client_configuration.rs @@ -1,5 +1,7 @@ -use graph_rs_sdk::header::HeaderMap; -use graph_rs_sdk::{Graph, GraphClientConfiguration}; +#![allow(dead_code, unused, unused_imports, clippy::module_inception)] +use graph_rs_sdk::{header::HeaderMap, header::HeaderValue, Graph, GraphClientConfiguration}; +use http::header::ACCEPT; +use http::HeaderName; use std::time::Duration; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; @@ -12,3 +14,20 @@ fn main() { let _ = Graph::from(client_config); } + +// Custom headers + +async fn per_request_headers() { + let client = Graph::new("token"); + + let _result = client + .users() + .list_user() + .header(ACCEPT, HeaderValue::from_static("*/*")) + .header( + HeaderName::from_static("HeaderName"), + HeaderValue::from_static("HeaderValue"), + ) + .send() + .await; +} diff --git a/examples/interactive_authentication/customize_webview.rs b/examples/interactive_authentication/customize_webview.rs index 6b0b73b6..870b49d1 100644 --- a/examples/interactive_authentication/customize_webview.rs +++ b/examples/interactive_authentication/customize_webview.rs @@ -16,6 +16,6 @@ async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, r .with_scope(scope) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(redirect_uri) - .interactive_authentication(Some(get_webview_options())) + .with_interactive_authentication(Some(get_webview_options())) .unwrap(); } diff --git a/examples/interactive_authentication/web_view.rs b/examples/interactive_authentication/web_view.rs index da8a9507..6abc5fcc 100644 --- a/examples/interactive_authentication/web_view.rs +++ b/examples/interactive_authentication/web_view.rs @@ -47,7 +47,7 @@ async fn authenticate() { .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(REDIRECT_URI) - .interactive_authentication(None) + .with_interactive_authentication(None) .unwrap(); let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); diff --git a/examples/oauth/auth_code_grant/interactive_authentication.rs b/examples/oauth/auth_code_grant/interactive_authentication.rs deleted file mode 100644 index d547854a..00000000 --- a/examples/oauth/auth_code_grant/interactive_authentication.rs +++ /dev/null @@ -1,70 +0,0 @@ -use graph_oauth::oauth::ConfidentialClientApplication; -use graph_rs_sdk::oauth::{ - web::Theme, web::WebViewOptions, AuthorizationCodeCredential, - AuthorizationCodeCredentialBuilder, TokenCredentialExecutor, -}; -use graph_rs_sdk::Graph; - -static CLIENT_ID: &str = "CLIENT_ID"; -static CLIENT_SECRET: &str = "CLIENT_SECRET"; -static TENANT_ID: &str = "TENANT_ID"; - -// This should be the user id for the user you are logging in as. -static USER_ID: &str = "USER_ID"; - -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; - -// Requires feature=interactive_authentication - -// Interactive Authentication WebView Using Wry library https://github.com/tauri-apps/wry -// See the wry documentation for platform specific installation. Linux and macOS require -// installation of platform specific dependencies. These are not included by default. - -// This example executes the Authorization Code OAuth flow and handles -// sign in/redirect using WebView as well as authorization and token retrieval. - -// The WebView window will load on the sign in page for Microsoft Graph -// Log in with a user and upon redirect the window will close automatically. -// The credential_builder will store the authorization code returned on the -// redirect url after logging in and then build a ConfidentialClient<AuthorizationCodeCredential> - -// The ConfidentialClient<AuthorizationCodeCredential> handles authorization to get an access token -// on the first request made using the Graph client. The token is stored in an in memory cache -// and subsequent calls will use this token. If a refresh token is included, which you can get -// by requesting the offline_access scope, then the confidential client will take care of refreshing -// the token. - -fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { - let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) - .auth_code_url_builder() - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .interactive_authentication(None) - .unwrap(); - - confidential_client_builder - .with_client_secret(CLIENT_SECRET) - .build() -} - -async fn authenticate() { - // Create a tracing subscriber to log debug/trace events coming from - // authorization http calls and the Graph client. - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - .with_max_level(tracing::Level::TRACE) - .init(); - - let confidential_client = run_interactive_auth(); - - let client = Graph::from(&confidential_client); - - let response = client.user(USER_ID).get_user().send().await.unwrap(); - - println!("{response:#?}"); - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); -} diff --git a/examples/oauth/auth_code_grant/mod.rs b/examples/oauth/auth_code_grant/mod.rs index 1ffebba6..37f66dca 100644 --- a/examples/oauth/auth_code_grant/mod.rs +++ b/examples/oauth/auth_code_grant/mod.rs @@ -1,6 +1,3 @@ pub mod auth_code_grant_pkce; pub mod auth_code_grant_secret; - pub mod server_examples; - -pub mod interactive_authentication; diff --git a/examples/oauth/client_credentials/client_credentials_secret.rs b/examples/oauth/client_credentials/client_credentials_secret.rs new file mode 100644 index 00000000..bcb05e40 --- /dev/null +++ b/examples/oauth/client_credentials/client_credentials_secret.rs @@ -0,0 +1,20 @@ +// This example shows using client credentials being passed to the Graph client which will +// handle access token refresh automatically. This example requires that admin consent +// has been granted to your app beforehand. If you have not granted admin consent, see +// examples/client_credentials_admin_consent.rs for more info. + +use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; + +// Replace client id, client secret, and tenant id with your own values. +static CLIENT_ID: &str = "<CLIENT_ID>"; +static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; +static TENANT_ID: &str = "<TENANT_ID>"; + +pub async fn get_graph_client() -> Graph { + let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) + .with_client_secret(CLIENT_SECRET) + .with_tenant(TENANT_ID) + .build(); + + Graph::from(&confidential_client_application) +} diff --git a/examples/oauth/client_credentials/mod.rs b/examples/oauth/client_credentials/mod.rs index d9123eec..4f3020d9 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/oauth/client_credentials/mod.rs @@ -10,26 +10,7 @@ // only has to be done once for a user. After admin consent is given, the oauth client can be // used to continue getting new access tokens programmatically. -mod client_credentials_admin_consent; - -pub use client_credentials_admin_consent::*; +mod client_credentials_secret; +mod server_examples; use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; - -// This example shows programmatically getting an access token using the client credentials -// flow after admin consent has been granted. If you have not granted admin consent, see -// examples/client_credentials_admin_consent.rs for more info. - -// Replace client id, client secret, and tenant id with your own values. -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static TENANT_ID: &str = "<TENANT_ID>"; - -pub async fn get_graph_client() -> Graph { - let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_tenant(TENANT_ID) - .build(); - - Graph::from(&confidential_client_application) -} diff --git a/examples/oauth/client_credentials/client_credentials_admin_consent.rs b/examples/oauth/client_credentials/server_examples/client_credentials_admin_consent.rs similarity index 100% rename from examples/oauth/client_credentials/client_credentials_admin_consent.rs rename to examples/oauth/client_credentials/server_examples/client_credentials_admin_consent.rs diff --git a/examples/oauth/client_credentials/server_examples/mod.rs b/examples/oauth/client_credentials/server_examples/mod.rs new file mode 100644 index 00000000..3a03bbd3 --- /dev/null +++ b/examples/oauth/client_credentials/server_examples/mod.rs @@ -0,0 +1 @@ +mod client_credentials_admin_consent; diff --git a/examples/oauth/getting_tokens_manually.rs b/examples/oauth/getting_tokens_manually.rs index f4a70fc3..9aa0d9f7 100644 --- a/examples/oauth/getting_tokens_manually.rs +++ b/examples/oauth/getting_tokens_manually.rs @@ -1,3 +1,4 @@ +use graph_core::identity::ClientApplication; use graph_rs_sdk::oauth::{ AuthorizationCodeCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; @@ -20,19 +21,36 @@ async fn auth_code_grant( let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: Token = response.json().await.unwrap(); - println!("{:#?}", access_token.access_token); + let token: Token = response.json().await.unwrap(); + println!("{:#?}", token.access_token); } // Client Credentials Grant async fn client_credentials() { let mut confidential_client = ConfidentialClientApplication::builder("CLIENT_ID") .with_client_secret("CLIENT_SECRET") + .with_tenant("TENANT_ID") .build(); let response = confidential_client.execute_async().await.unwrap(); println!("{response:#?}"); - let access_token: Token = response.json().await.unwrap(); - println!("{:#?}", access_token.access_token); + let token: Token = response.json().await.unwrap(); + println!("{:#?}", token.access_token); +} + +// Use get_token_silent and get_token_silent_async to have the +// credential client check the in memory token cache before making +// an http request to get a new token. + +// The execute and execute_async methods do not store or retrieve any +// tokens from the cache. +async fn using_token_cache() { + let mut confidential_client = ConfidentialClientApplication::builder("CLIENT_ID") + .with_client_secret("CLIENT_SECRET") + .with_tenant("TENANT_ID") + .build(); + + let access_token = confidential_client.get_token_silent_async().await.unwrap(); + println!("{access_token:#?}"); } diff --git a/examples/paging_and_next_links.rs b/examples/paging_and_next_links.rs index 186a79f3..4b2f0e23 100644 --- a/examples/paging_and_next_links.rs +++ b/examples/paging_and_next_links.rs @@ -5,6 +5,8 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; +// See examples/paging for more examples. + #[derive(Debug, Serialize, Deserialize)] pub struct User { pub(crate) id: Option<String>, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index cc7006a0..6cb834eb 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -49,12 +49,15 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// to build the url that the user will be directed to authorize at. /// /// ```rust -/// # use graph_oauth::identity::ConfidentialClientApplication;/// +/// # use graph_oauth::oauth::{ConfidentialClientApplication, Prompt}; /// /// let client_app = ConfidentialClientApplication::builder("client-id") /// .auth_code_url_builder() -/// .with_client_secret("client-secret") +/// .with_tenant("tenant-id") +/// .with_prompt(Prompt::Login) +/// .with_state("1234") /// .with_scope(vec!["User.Read"]) +/// .with_redirect_uri("http://localhost:8000") /// .build(); /// /// ``` @@ -249,7 +252,6 @@ pub(crate) mod web_view_authenticator { let uri = self.url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); - let _timeout = web_view_options.timeout; let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { @@ -556,7 +558,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - pub fn interactive_authentication( + pub fn with_interactive_authentication( &self, options: Option<WebViewOptions>, ) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { diff --git a/src/lib.rs b/src/lib.rs index 215be336..735bdb54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,13 +162,6 @@ //! //! ### Supported Authorization Flows //! -//! #### Microsoft OneDrive and SharePoint -//! -//! - [Token Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#token-flow) -//! - [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) -//! -//! #### Microsoft Identity Platform -//! //! - [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) //! - [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) //! - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) From 9ae0f5d5f2e9f26ede56ef05db51ec9d0e6724ea Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 5 Nov 2023 04:38:45 -0500 Subject: [PATCH 056/118] Move scope to AppConfig --- README.md | 3 +- examples/interactive_authentication/README.md | 97 ++++++++++++++ .../customize_webview.rs | 21 --- examples/interactive_authentication/main.rs | 5 +- .../{web_view.rs => webview.rs} | 3 +- .../webview_errors.rs | 44 ++++++ .../webview_options.rs | 40 ++++++ examples/oauth/device_code.rs | 2 +- .../legacy/implicit_grant.rs | 10 +- graph-core/src/cache/cache_store.rs | 10 ++ graph-core/src/cache/in_memory_cache_store.rs | 9 +- graph-core/src/cache/mod.rs | 2 + graph-error/src/authorization_failure.rs | 17 +-- graph-error/src/lib.rs | 3 +- graph-error/src/webview_error.rs | 39 ++++++ graph-oauth/src/auth.rs | 18 +++ .../src/identity/credentials/app_config.rs | 126 +++++++++++------- .../credentials/application_builder.rs | 62 +-------- .../auth_code_authorization_url.rs | 69 +++------- ...authorization_code_assertion_credential.rs | 16 +-- ...thorization_code_certificate_credential.rs | 12 +- .../authorization_code_credential.rs | 55 +++----- .../client_assertion_credential.rs | 46 +++---- .../credentials/client_builder_impl.rs | 22 ++- .../client_certificate_credential.rs | 38 ++---- .../client_credentials_authorization_url.rs | 12 +- .../credentials/client_secret_credential.rs | 50 ++++--- .../confidential_client_application.rs | 74 ---------- .../credentials/device_code_credential.rs | 64 ++++----- .../credentials/legacy/implicit_credential.rs | 85 +++++------- .../credentials/open_id_authorization_url.rs | 92 ++++++------- .../credentials/open_id_credential.rs | 47 +++---- .../resource_owner_password_credential.rs | 67 +++++----- graph-oauth/src/lib.rs | 11 +- .../src/web/interactive_authenticator.rs | 4 +- graph-oauth/src/web/interactive_web_view.rs | 15 ++- graph-oauth/src/web/web_view_options.rs | 32 ++++- src/lib.rs | 8 +- tests/reports_request.rs | 3 + tests/upload_request_blocking.rs | 2 +- 40 files changed, 673 insertions(+), 662 deletions(-) create mode 100644 examples/interactive_authentication/README.md delete mode 100644 examples/interactive_authentication/customize_webview.rs rename examples/interactive_authentication/{web_view.rs => webview.rs} (94%) create mode 100644 examples/interactive_authentication/webview_errors.rs create mode 100644 examples/interactive_authentication/webview_options.rs create mode 100644 graph-core/src/cache/cache_store.rs create mode 100644 graph-error/src/webview_error.rs diff --git a/README.md b/README.md index 545a393c..0a235f3c 100644 --- a/README.md +++ b/README.md @@ -1154,8 +1154,7 @@ fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCred let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) .auth_code_url_builder() .with_tenant(TENANT_ID) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(REDIRECT_URI) .interactive_authentication(None) .unwrap(); diff --git a/examples/interactive_authentication/README.md b/examples/interactive_authentication/README.md new file mode 100644 index 00000000..f382325b --- /dev/null +++ b/examples/interactive_authentication/README.md @@ -0,0 +1,97 @@ +# Interactive Authentication + +Interactive Authentication uses a webview to perform sign in and handle the redirect +uri making it easy for you to integrate the sdk into your application. + +The sdk uses the wry crate internally to provide a webview. + +### Example + +```rust +static CLIENT_ID: &str = "CLIENT_ID"; +static CLIENT_SECRET: &str = "CLIENT_SECRET"; +static TENANT_ID: &str = "TENANT_ID"; + +// This should be the user id for the user you are logging in as. +static USER_ID: &str = "USER_ID"; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +async fn authenticate() { + // Create a tracing subscriber to log debug/trace events coming from + // authorization http calls and the Graph client. + tracing_subscriber::fmt() + .pretty() + .with_thread_names(true) + .with_max_level(tracing::Level::TRACE) + .init(); + + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .with_interactive_authentication(None) + .unwrap(); + + let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); + + let client = Graph::from(&confidential_client); + + let response = client.user(USER_ID).get_user().send().await.unwrap(); + + println!("{response:#?}"); + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); +} + +``` + + +### WebView Options + +You can customize several aspects of the webview including security mechanisms +or setting an OS theme. + +```rust +use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use std::ops::Add; +use std::time::{Duration, Instant}; + +fn get_webview_options() -> WebViewOptions { + WebViewOptions::builder() + // Give the window a title. The default is "Sign In" + .with_window_title("Sign In") + // OS specific theme. Does not work on all operating systems. + // See wry crate for more info. + .with_theme(Theme::Dark) + // Close the webview window whenever there is a navigation by the webview or user + // to a url that is not one of the redirect urls or the login url. + // For instance, if this is considered a security issue and the user should + // not be able to navigate to another url. + // Either way, the url bar does not show regardless. + .with_close_window_on_invalid_navigation(true) + // Add a timeout that will close the window and return an error + // when that timeout is reached. For instance, if your app is waiting on the + // user to log in and the user has not logged in after 20 minutes you may + // want to assume the user is idle in some way and close out of the webview window. + .with_timeout(Instant::now().add(Duration::from_secs(1200))) + // The webview can store the cookies that were set after sign in so that on the next + // sign in the user is automatically logged in through SSO. Or you can clear the browsing + // data, cookies in this case, after sign in when the webview window closes. + .with_clear_browsing_data(false) + // Provide a list of ports to use for interactive authentication. + // This assumes that you have http://localhost or http://localhost:port + // for each port registered in your ADF application registration. + .with_ports(&[8000]) +} + +async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_interactive_authentication(Some(get_webview_options())) + .unwrap(); +} +``` diff --git a/examples/interactive_authentication/customize_webview.rs b/examples/interactive_authentication/customize_webview.rs deleted file mode 100644 index 870b49d1..00000000 --- a/examples/interactive_authentication/customize_webview.rs +++ /dev/null @@ -1,21 +0,0 @@ -use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; -use std::ops::Add; -use std::time::{Duration, Instant}; - -fn get_webview_options() -> WebViewOptions { - WebViewOptions::builder() - .with_window_title("Sign In") - .with_theme(Theme::Dark) - .with_close_window_on_invalid_navigation(true) - .with_timeout(Instant::now().add(Duration::from_secs(120))) -} - -async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(scope) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri) - .with_interactive_authentication(Some(get_webview_options())) - .unwrap(); -} diff --git a/examples/interactive_authentication/main.rs b/examples/interactive_authentication/main.rs index 918b4e82..ebf0ccda 100644 --- a/examples/interactive_authentication/main.rs +++ b/examples/interactive_authentication/main.rs @@ -1,6 +1,7 @@ #![allow(dead_code, unused, unused_imports)] +mod webview; -mod customize_webview; -mod web_view; +mod webview_errors; +mod webview_options; fn main() {} diff --git a/examples/interactive_authentication/web_view.rs b/examples/interactive_authentication/webview.rs similarity index 94% rename from examples/interactive_authentication/web_view.rs rename to examples/interactive_authentication/webview.rs index 6abc5fcc..2506cfe9 100644 --- a/examples/interactive_authentication/web_view.rs +++ b/examples/interactive_authentication/webview.rs @@ -44,8 +44,7 @@ async fn authenticate() { let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT_ID) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(REDIRECT_URI) .with_interactive_authentication(None) .unwrap(); diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs new file mode 100644 index 00000000..e90a3b8c --- /dev/null +++ b/examples/interactive_authentication/webview_errors.rs @@ -0,0 +1,44 @@ +use anyhow::Error; +use graph_error::WebViewExecutionError; +use graph_oauth::oauth::AuthorizationCodeCredential; + +async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { + let mut credential_builder_result = + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_interactive_authentication(None); + + if let Ok(credential_builder) = credential_builder_result { + // ... + } else if let Err(err) = credential_builder_result { + match err { + // Issues with the redirect uri such as specifying localhost + // but not providing a port in the WebViewOptions. + WebViewExecutionError::InvalidRedirectUri(uri) => {} + // The user closed the webview window without logging in. + WebViewExecutionError::WindowClosedRequested => {} + // The user navigated to a url that was not the login url + // or a redirect url specified. Requires that WebViewOptions + // has the enforcement of invalid navigation enabled. + WebViewExecutionError::WindowClosedOnInvalidNavigation => {} + // The webview exited because of a timeout defined in the WebViewOptions. + WebViewExecutionError::WindowClosedOnTimeoutReached => {} + // The host or domain provided or set for login is invalid. + // This could be an internal error and most likely will never happen. + WebViewExecutionError::InvalidStartUri { reason } => {} + // The webview was successfully redirected but the url did not + // contain a query or fragment. The query or fragment of the url + // is where the auth code would be returned to the app. + WebViewExecutionError::RedirectUriMissingQueryOrFragment(_) => {} + // Serde serialization error when attempting to serialize + // the query or fragment of the url to a AuthorizationQueryResponse + WebViewExecutionError::SerdeError(_) => {} + // Error from AuthorizationCodeCredential Authorization Url Builder: AuthCodeAuthorizationUrlParameters + // This most likely came from an invalid parameter or missing parameter + // passed to the client used for building the url. See graph_rs_sdk::oauth::AuthCodeAuthorizationUrlParameters + WebViewExecutionError::AuthorizationError(authorization_failure) => {} + } + } +} diff --git a/examples/interactive_authentication/webview_options.rs b/examples/interactive_authentication/webview_options.rs new file mode 100644 index 00000000..cf2cf913 --- /dev/null +++ b/examples/interactive_authentication/webview_options.rs @@ -0,0 +1,40 @@ +use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use std::ops::Add; +use std::time::{Duration, Instant}; + +fn get_webview_options() -> WebViewOptions { + WebViewOptions::builder() + // Give the window a title. The default is "Sign In" + .with_window_title("Sign In") + // OS specific theme. Does not work on all operating systems. + // See wry crate for more info. + .with_theme(Theme::Dark) + // Close the webview window whenever there is a navigation by the webview or user + // to a url that is not one of the redirect urls or the login url. + // For instance, if this is considered a security issue and the user should + // not be able to navigate to another url. + // Either way, the url bar does not show regardless. + .with_close_window_on_invalid_navigation(true) + // Add a timeout that will close the window and return an error + // when that timeout is reached. For instance, if your app is waiting on the + // user to log in and the user has not logged in after 20 minutes you may + // want to assume the user is idle in some way and close out of the webview window. + .with_timeout(Instant::now().add(Duration::from_secs(1200))) + // The webview can store the cookies that were set after sign in so that on the next + // sign in the user is automatically logged in through SSO. Or you can clear the browsing + // data, cookies in this case, after sign in when the webview window closes. + .with_clear_browsing_data(false) + // Provide a list of ports to use for interactive authentication. + // This assumes that you have http://localhost or http://localhost:port + // for each port registered in your ADF application registration. + .with_ports(&[8000]) +} + +async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { + let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_interactive_authentication(Some(get_webview_options())) + .unwrap(); +} diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index 70f157ef..daebf929 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -21,7 +21,7 @@ static TENANT: &str = "<TENANT>"; // device code endpoint is polled an access token is returned. fn poll_device_code() { let mut device_executor = PublicClientApplication::builder(CLIENT_ID) - .with_device_code_authorization_executor() + .with_device_code_polling_executor() .with_scope(vec!["User.Read"]) .poll() .unwrap(); diff --git a/examples/oauth_authorization_url/legacy/implicit_grant.rs b/examples/oauth_authorization_url/legacy/implicit_grant.rs index 475f5bae..34ac3338 100644 --- a/examples/oauth_authorization_url/legacy/implicit_grant.rs +++ b/examples/oauth_authorization_url/legacy/implicit_grant.rs @@ -26,8 +26,8 @@ use graph_rs_sdk::oauth::legacy::ImplicitCredential; use graph_rs_sdk::oauth::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; fn oauth_implicit_flow() { - let authorizer = ImplicitCredential::builder() - .with_client_id("<YOUR_CLIENT_ID>") + let authorizer = ImplicitCredential::builder("<YOUR_CLIENT_ID>") + .unwrap() .with_prompt(Prompt::Login) .with_response_type(ResponseType::Token) .with_response_mode(ResponseMode::Fragment) @@ -48,13 +48,15 @@ fn oauth_implicit_flow() { } fn multi_response_types() { - let _ = ImplicitCredential::builder() + let _ = ImplicitCredential::builder("<YOUR_CLIENT_ID>") + .unwrap() .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) .build(); // Or - let _ = ImplicitCredential::builder() + let _ = ImplicitCredential::builder("<YOUR_CLIENT_ID>") + .unwrap() .with_response_type(ResponseType::StringSet(BTreeSet::from_iter(vec![ "token".to_string(), "id_token".to_string(), diff --git a/graph-core/src/cache/cache_store.rs b/graph-core/src/cache/cache_store.rs new file mode 100644 index 00000000..19818e34 --- /dev/null +++ b/graph-core/src/cache/cache_store.rs @@ -0,0 +1,10 @@ +pub trait CacheStore<Value> { + /// Store Value given cache id. + fn store<T: Into<String>>(&mut self, cache_id: T, token: Value); + + /// Get Value from cache given matching cache id. + fn get(&self, cache_id: &str) -> Option<Value>; + + /// Evict or remove value from cache given cache id. + fn evict(&self, cache_id: &str) -> Option<Value>; +} diff --git a/graph-core/src/cache/in_memory_cache_store.rs b/graph-core/src/cache/in_memory_cache_store.rs index e524f726..5762b96d 100644 --- a/graph-core/src/cache/in_memory_cache_store.rs +++ b/graph-core/src/cache/in_memory_cache_store.rs @@ -1,3 +1,4 @@ +use crate::cache::cache_store::CacheStore; use std::collections::HashMap; use std::sync::{Arc, RwLock}; @@ -12,21 +13,23 @@ impl<Value: Clone> InMemoryCacheStore<Value> { store: Default::default(), } } +} - pub fn store<T: Into<String>>(&mut self, cache_id: T, token: Value) { +impl<Value: Clone> CacheStore<Value> for InMemoryCacheStore<Value> { + fn store<T: Into<String>>(&mut self, cache_id: T, token: Value) { let mut write_lock = self.store.write().unwrap(); write_lock.insert(cache_id.into(), token); drop(write_lock); } - pub fn get(&self, cache_id: &str) -> Option<Value> { + fn get(&self, cache_id: &str) -> Option<Value> { let read_lock = self.store.read().unwrap(); let token = read_lock.get(cache_id).cloned(); drop(read_lock); token } - pub fn evict(&self, cache_id: &str) -> Option<Value> { + fn evict(&self, cache_id: &str) -> Option<Value> { let mut write_lock = self.store.write().unwrap(); let token = write_lock.remove(cache_id); drop(write_lock); diff --git a/graph-core/src/cache/mod.rs b/graph-core/src/cache/mod.rs index 1c60c412..635b9e48 100644 --- a/graph-core/src/cache/mod.rs +++ b/graph-core/src/cache/mod.rs @@ -1,5 +1,7 @@ +mod cache_store; mod in_memory_cache_store; mod token_cache; +pub use cache_store::*; pub use in_memory_cache_store::*; pub use token_cache::*; diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index bcca4c8b..31e56a3f 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -3,6 +3,8 @@ use tokio::sync::mpsc::error::SendTimeoutError; pub type AF = AuthorizationFailure; +/// Errors typically from missing or invalid configuration using one of the +/// identity platform clients such as AuthorizationCodeCredential. #[derive(Debug, thiserror::Error)] pub enum AuthorizationFailure { #[error("Required value missing:\n{0:#?}", name)] @@ -99,6 +101,9 @@ impl AuthorizationFailure { } } +/// Error either from missing or invalid configuration using one of the +/// identity platform clients or an error from the result of executing +/// an http request using the identity platform clients. #[derive(Debug, thiserror::Error)] pub enum AuthExecutionError { #[error("{0:#?}")] @@ -120,15 +125,3 @@ pub enum AuthTaskExecutionError<R> { #[error("{0:#?}")] JoinError(#[from] tokio::task::JoinError), } - -#[derive(Debug, thiserror::Error)] -pub enum WebViewExecutionError { - #[error("TimedOut")] - TimedOut, - #[error("InvalidNavigation")] - InvalidNavigation, - #[error("InvalidRedirectUri")] - InvalidRedirectUri, - #[error("WindowCloseRequested")] - WindowCloseRequested, -} diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index b553a768..40d3abe9 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -9,15 +9,16 @@ mod error; mod graph_failure; mod internal; pub mod io_error; +mod webview_error; pub use authorization_failure::*; pub use error::*; pub use graph_failure::*; pub use internal::*; +pub use webview_error::*; pub type GraphResult<T> = Result<T, GraphFailure>; pub type IdentityResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; - pub type WebViewResult<T> = Result<T, WebViewExecutionError>; diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs new file mode 100644 index 00000000..b6d99010 --- /dev/null +++ b/graph-error/src/webview_error.rs @@ -0,0 +1,39 @@ +use crate::AuthorizationFailure; +use url::ParseError; + +#[derive(Debug, thiserror::Error)] +pub enum WebViewExecutionError { + // Issues with the redirect uri such as specifying localhost + // but not providing a port in the WebViewOptions. + #[error("InvalidRedirectUri: {0:#?}")] + InvalidRedirectUri(String), + /// The user closed the webview window without logging in. + #[error("WindowClosedRequested")] + WindowClosedRequested, + /// The user navigated to a url that was not the login url + /// or a redirect url specified. Requires that WebViewOptions + /// has the enforcement of invalid navigation enabled. + #[error("WindowClosedOnInvalidNavigation")] + WindowClosedOnInvalidNavigation, + /// The webview exited because of a timeout defined in the WebViewOptions. + #[error("WindowClosedOnTimeoutReached")] + WindowClosedOnTimeoutReached, + /// The host or domain provided or set for login is invalid. + /// This could be an internal error and most likely will never happen. + #[error("InvalidStartUri: {reason:#?}")] + InvalidStartUri { reason: String }, + /// The webview was successfully redirected but the url did not + /// contain a query or fragment. The query or fragment of the url + /// is where the auth code would be returned to the app. + #[error("No query or fragment returned on redirect uri: {0:#?}")] + RedirectUriMissingQueryOrFragment(String), + /// Serde serialization error when attempting to serialize + /// the query or fragment of the url to a AuthorizationQueryResponse + #[error("{0:#?}")] + SerdeError(#[from] serde::de::value::Error), + /// Error from building out the parameters necessary for authorization + /// this most likely came from an invalid parameter or missing parameter + /// passed to the client used for building the url. + #[error("{0:#?}")] + AuthorizationError(#[from] AuthorizationFailure), +} diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/auth.rs index 2534f233..78b21c31 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/auth.rs @@ -712,6 +712,24 @@ impl OAuthSerializer { .join(sep) } + /// Set scope. Overriding all previous scope values. + /// + /// # Example + /// ``` + /// # use graph_oauth::extensions::OAuthSerializer; + /// # use std::collections::HashSet; + /// # let mut oauth = OAuthSerializer::new(); + /// + /// let scopes = vec!["Files.Read", "Files.ReadWrite"]; + /// oauth.extend_scopes(&scopes); + /// + /// assert_eq!(oauth.join_scopes(" "), "Files.Read Files.ReadWrite"); + /// ``` + pub fn set_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, iter: I) -> &mut Self { + self.scopes = iter.into_iter().map(|s| s.to_string()).collect(); + self + } + /// Extend scopes. /// /// # Example diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index a6e0909c..9a0e6824 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,5 +1,5 @@ use base64::Engine; -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; use graph_error::AF; @@ -29,6 +29,31 @@ pub struct AppConfig { pub(crate) azure_cloud_instance: AzureCloudInstance, pub(crate) extra_query_parameters: HashMap<String, String>, pub(crate) extra_header_parameters: HeaderMap, + /// Required - + /// A space-separated list of scopes. You might also include + /// other scopes in this request for requesting consent. + /// + /// For OpenID Connect, it must include the scope openid, which translates to the Sign you in + /// permission in the consent UI. Openid scope is already included for [OpenIdCredential](crate::identity::OpenIdCredential) + /// and for [OpenIdAuthorizationUrlParameters](crate::identity::OpenIdAuthorizationUrlParameters) + /// + /// For Client Credentials, The value passed for the scope parameter in this request should + /// be the resource identifier (application ID URI) of the resource you want, affixed with + /// the .default suffix. All scopes included must be for a single resource. + /// Including scopes for multiple resources will result in an error. + /// + /// For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. + /// This value tells the Microsoft identity platform that of all the direct application + /// permissions you have configured for your app, the endpoint should issue a token for the + /// ones associated with the resource you want to use. To learn more about the /.default scope, + /// see the [consent documentation](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#the-default-scope). + /// + /// This https://graph.microsoft.com/.default scope is automatically set for + /// [ClientCredentialsAuthorizationUrlParameters](crate::identity::ClientCredentialsAuthorizationUrlParameters), + /// [ClientSecretCredential](crate::identity::ClientSecretCredential), + /// [ClientCertificateCredential](crate::identity::ClientCertificateCredential), + /// and [ClientAssertionCredential](crate::identity::ClientAssertionCredential). + pub(crate) scope: BTreeSet<String>, /// Optional - Some flows may require the redirect URI /// The redirect_uri of your app, where authentication responses can be sent and received /// by your app. It must exactly match one of the redirect_uris you registered in the portal, @@ -56,6 +81,7 @@ impl TryFrom<ApplicationOptions> for AppConfig { azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), + scope: Default::default(), redirect_uri: None, cache_id, force_token_refresh: Default::default(), @@ -74,6 +100,7 @@ impl Debug for AppConfig { .field("azure_cloud_instance", &self.azure_cloud_instance) .field("extra_query_parameters", &self.extra_query_parameters) .field("extra_header_parameters", &self.extra_header_parameters) + .field("scope", &self.scope) .field("force_token_refresh", &self.force_token_refresh) .finish() } else { @@ -83,6 +110,7 @@ impl Debug for AppConfig { .field("authority", &self.authority) .field("azure_cloud_instance", &self.azure_cloud_instance) .field("extra_query_parameters", &self.extra_query_parameters) + .field("scope", &self.scope) .field("force_token_refresh", &self.force_token_refresh) .finish() } @@ -101,35 +129,13 @@ impl AppConfig { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()) } } - pub(crate) fn new_init( - client_id: Uuid, - tenant: Option<impl AsRef<str>>, - redirect_uri: Option<Url>, - ) -> AppConfig { - let tenant_id: Option<String> = tenant.map(|value| value.as_ref().to_string()); - let cache_id = AppConfig::generate_cache_id(client_id, tenant_id.as_ref()); - - let authority = tenant_id - .clone() - .map(Authority::TenantId) - .unwrap_or_default(); - AppConfig { - tenant_id, - client_id, - authority, - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri, - cache_id, - force_token_refresh: Default::default(), - log_pii: Default::default(), - } + pub(crate) fn builder(client_id: impl TryInto<Uuid>) -> AppConfigBuilder { + AppConfigBuilder::new(client_id) } - pub(crate) fn new_with_client_id(client_id: impl AsRef<str>) -> AppConfig { - let client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); + pub(crate) fn new(client_id: impl TryInto<Uuid>) -> AppConfig { + let client_id = client_id.try_into().unwrap_or_default(); let cache_id = AppConfig::generate_cache_id(client_id, None); AppConfig { @@ -139,6 +145,7 @@ impl AppConfig { azure_cloud_instance: Default::default(), extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), + scope: Default::default(), redirect_uri: None, cache_id, force_token_refresh: Default::default(), @@ -146,33 +153,50 @@ impl AppConfig { } } - pub(crate) fn new_with_tenant_and_client_id( - tenant_id: impl AsRef<str>, - client_id: impl AsRef<str>, - ) -> AppConfig { - let client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); - let tenant_id = tenant_id.as_ref(); - let cache_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - client_id.to_string() - )); + pub fn log_pii(&mut self, log_pii: bool) { + self.log_pii = log_pii; + } +} - AppConfig { - tenant_id: Some(tenant_id.to_string()), - client_id, - authority: Authority::TenantId(tenant_id.to_string()), - azure_cloud_instance: Default::default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: None, - cache_id, - force_token_refresh: Default::default(), - log_pii: Default::default(), +#[derive(Clone, Default, PartialEq)] +pub struct AppConfigBuilder { + app_config: AppConfig, +} + +impl AppConfigBuilder { + pub fn new(client_id: impl TryInto<Uuid>) -> AppConfigBuilder { + AppConfigBuilder { + app_config: AppConfig::new(client_id), } } - pub fn log_pii(&mut self, log_pii: bool) { - self.log_pii = log_pii; + pub fn tenant(mut self, tenant: impl Into<String>) -> Self { + let tenant_id = tenant.into(); + self.app_config.tenant_id = Some(tenant_id.clone()); + self.authority(Authority::TenantId(tenant_id)) + } + + pub fn redirect_uri(mut self, redirect_uri: Url) -> Self { + self.app_config.redirect_uri = Some(redirect_uri); + self + } + + pub fn redirect_uri_option(mut self, redirect_uri: Option<Url>) -> Self { + self.app_config.redirect_uri = redirect_uri; + self + } + + pub fn authority(mut self, authority: Authority) -> Self { + self.app_config.authority = authority; + self + } + + pub fn scope<T: ToString, I: IntoIterator<Item = T>>(mut self, scope: I) -> Self { + self.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + + pub fn build(self) -> AppConfig { + self.app_config } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 882ee862..8d84fc86 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -28,7 +28,7 @@ pub struct ConfidentialClientApplicationBuilder { impl ConfidentialClientApplicationBuilder { pub fn new(client_id: impl AsRef<str>) -> Self { ConfidentialClientApplicationBuilder { - app_config: AppConfig::new_with_client_id(client_id), + app_config: AppConfig::new(client_id.as_ref()), } } @@ -212,34 +212,8 @@ impl TryFrom<ApplicationOptions> for ConfidentialClientApplicationBuilder { "Both represent an authority audience and cannot be set at the same time", )?; - let cache_id = { - if let Some(tenant_id) = value.tenant_id.as_ref() { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - value.client_id.to_string() - )) - } else { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value.client_id.to_string()) - } - }; - Ok(ConfidentialClientApplicationBuilder { - app_config: AppConfig { - tenant_id: value.tenant_id, - client_id: Uuid::try_parse(&value.client_id.to_string()).unwrap_or_default(), - authority: value - .aad_authority_audience - .map(Authority::from) - .unwrap_or_default(), - azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: None, - cache_id, - force_token_refresh: Default::default(), - log_pii: Default::default(), - }, + app_config: AppConfig::try_from(value)?, }) } } @@ -253,7 +227,7 @@ impl PublicClientApplicationBuilder { #[allow(dead_code)] pub fn new(client_id: impl AsRef<str>) -> PublicClientApplicationBuilder { PublicClientApplicationBuilder { - app_config: AppConfig::new_with_client_id(client_id), + app_config: AppConfig::new(client_id.as_ref()), } } @@ -314,7 +288,7 @@ impl PublicClientApplicationBuilder { self } - pub fn with_device_code_authorization_executor(self) -> DeviceCodePollingExecutor { + pub fn with_device_code_polling_executor(self) -> DeviceCodePollingExecutor { DeviceCodePollingExecutor::new_with_app_config(self.app_config) } @@ -366,34 +340,8 @@ impl TryFrom<ApplicationOptions> for PublicClientApplicationBuilder { "TenantId and AadAuthorityAudience both represent an authority audience and cannot be set at the same time", )?; - let cache_id = { - if let Some(tenant_id) = value.tenant_id.as_ref() { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - value.client_id.to_string() - )) - } else { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(value.client_id.to_string()) - } - }; - Ok(PublicClientApplicationBuilder { - app_config: AppConfig { - tenant_id: value.tenant_id, - client_id: value.client_id, - authority: value - .aad_authority_audience - .map(Authority::from) - .unwrap_or_default(), - azure_cloud_instance: value.azure_cloud_instance.unwrap_or_default(), - extra_query_parameters: Default::default(), - extra_header_parameters: Default::default(), - redirect_uri: None, - cache_id, - force_token_refresh: Default::default(), - log_pii: Default::default(), - }, + app_config: AppConfig::try_from(value)?, }) } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 6cb834eb..9367f22c 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -8,7 +8,7 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; -use graph_error::{IdentityResult, AF}; +use graph_error::{IdentityResult, WebViewExecutionError, WebViewResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; @@ -85,11 +85,6 @@ pub struct AuthCodeAuthorizationUrlParameters { /// The nonce is automatically generated unless set by the caller. pub(crate) nonce: Option<String>, pub(crate) state: Option<String>, - /// Required. - /// A space-separated list of scopes that you want the user to consent to. - /// For the /authorize leg of the request, this parameter can cover multiple resources. - /// This value allows your app to get consent for multiple web APIs you want to call. - pub(crate) scope: Vec<String>, /// Optional /// Indicates the type of user interaction that is required. The only valid values at /// this time are login, none, consent, and select_account. @@ -131,7 +126,6 @@ impl Debug for AuthCodeAuthorizationUrlParameters { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("AuthCodeAuthorizationUrlParameters") .field("app_config", &self.app_config) - .field("scope", &self.scope) .field("response_type", &self.response_type) .field("response_mode", &self.response_mode) .field("prompt", &self.prompt) @@ -149,16 +143,13 @@ impl AuthCodeAuthorizationUrlParameters { let redirect_uri_result = Url::parse(redirect_uri.as_str()); Ok(AuthCodeAuthorizationUrlParameters { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - Some(redirect_uri.into_url().or(redirect_uri_result)?), - ), + app_config: AppConfig::builder(client_id.as_ref()) + .redirect_uri(redirect_uri.into_url().or(redirect_uri_result)?) + .build(), response_type, response_mode: None, nonce: None, state: None, - scope: vec![], prompt: None, domain_hint: None, login_hint: None, @@ -193,7 +184,7 @@ impl AuthCodeAuthorizationUrlParameters { pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, - ) -> anyhow::Result<AuthorizationQueryResponse> { + ) -> WebViewResult<AuthorizationQueryResponse> { let receiver = self.interactive_authentication(interactive_web_view_options)?; let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -203,17 +194,15 @@ impl AuthCodeAuthorizationUrlParameters { } return match next { - None => Err(anyhow::anyhow!("Unknown")), + None => unreachable!(), Some(auth_event) => match auth_event { InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + Err(WebViewExecutionError::InvalidRedirectUri(reason)) } InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; + let query = uri.query().or(uri.fragment()).ok_or( + WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), + )?; let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; @@ -221,14 +210,16 @@ impl AuthCodeAuthorizationUrlParameters { } InteractiveAuthEvent::ClosingWindow(window_close_reason) => { match window_close_reason { - WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), + WindowCloseReason::CloseRequested => { + Err(WebViewExecutionError::WindowClosedRequested) + } WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) + Err(WebViewExecutionError::WindowClosedOnInvalidNavigation) } WindowCloseReason::TimedOut { start: _, requested_resume: _, - } => Err(anyhow::anyhow!("TimedOut")), + } => Err(WebViewExecutionError::WindowClosedOnTimeoutReached), } } }, @@ -242,13 +233,13 @@ pub(crate) mod web_view_authenticator { use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, }; - use graph_error::IdentityResult; + use graph_error::WebViewResult; impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { fn interactive_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, - ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { + ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { let uri = self.url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); @@ -297,20 +288,13 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { return AF::result("client_id"); } - if self.scope.is_empty() { + if self.app_config.scope.is_empty() { return AF::result("scope"); } - if self.scope.contains(&String::from("openid")) { - return AF::msg_result( - "openid", - "Scope openid is not valid for authorization code - instead use OpenIdCredential", - ); - } - serializer .client_id(client_id.as_str()) - .extend_scopes(self.scope.clone()) + .set_scope(self.app_config.scope.clone()) .authority(azure_cloud_instance, &self.app_config.authority); let response_types: Vec<String> = @@ -408,12 +392,11 @@ impl AuthCodeAuthorizationUrlParameterBuilder { response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { credential: AuthCodeAuthorizationUrlParameters { - app_config: AppConfig::new_with_client_id(client_id.as_ref()), + app_config: AppConfig::new(client_id.as_ref()), response_mode: None, response_type, nonce: None, state: None, - scope: vec![], prompt: None, domain_hint: None, login_hint: None, @@ -435,7 +418,6 @@ impl AuthCodeAuthorizationUrlParameterBuilder { response_type, nonce: None, state: None, - scope: vec![], prompt: None, domain_hint: None, login_hint: None, @@ -495,15 +477,6 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - /// Adds the `offline_access` scope parameter which tells the authorization server - /// to include a refresh token in the redirect uri query. - pub fn with_offline_access(&mut self) -> &mut Self { - self.credential - .scope - .extend(vec!["offline_access".to_owned()]); - self - } - /// Indicates the type of user interaction that is required. Valid values are login, none, /// consent, and select_account. /// @@ -561,7 +534,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { pub fn with_interactive_authentication( &self, options: Option<WebViewOptions>, - ) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { + ) -> WebViewResult<AuthorizationCodeCredentialBuilder> { let query_response = self .credential .interactive_webview_authentication(options)?; diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index cc2d0b11..9ab23421 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -7,7 +7,7 @@ use reqwest::IntoUrl; use uuid::Uuid; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -84,11 +84,9 @@ impl AuthorizationCodeAssertionCredential { }; Ok(AuthorizationCodeAssertionCredential { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - redirect_uri, - ), + app_config: AppConfig::builder(client_id.as_ref()) + .redirect_uri_option(redirect_uri) + .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, code_verifier: None, @@ -105,11 +103,7 @@ impl AuthorizationCodeAssertionCredential { authorization_code: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( - AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - None, - ), + AppConfig::new(client_id.as_ref()), authorization_code, ) } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 200f0046..f3edc865 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -7,7 +7,7 @@ use reqwest::IntoUrl; use uuid::Uuid; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -84,11 +84,9 @@ impl AuthorizationCodeCertificateCredential { }; Ok(AuthorizationCodeCertificateCredential { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - redirect_uri, - ), + app_config: AppConfig::builder(client_id.as_ref()) + .redirect_uri_option(redirect_uri) + .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, code_verifier: None, @@ -107,7 +105,7 @@ impl AuthorizationCodeCertificateCredential { x509: &X509Certificate, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - AppConfig::new_with_client_id(client_id), + AppConfig::new(client_id.as_ref()), authorization_code, x509, ) diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 2e1651c5..ac6aae65 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -7,12 +7,12 @@ use reqwest::IntoUrl; use url::Url; use uuid::Uuid; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::crypto::ProofKeyCodeExchange; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, @@ -51,12 +51,6 @@ pub struct AuthorizationCodeCredential { /// specification. The Basic auth pattern of instead providing credentials in the Authorization /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, - /// A space-separated list of scopes. The scopes must all be from a single resource, - /// along with OIDC scopes (profile, openid, email). For more information, see Permissions - /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension - /// to the authorization code flow, intended to allow apps to declare the resource they want - /// the token for during token redemption. - pub(crate) scope: Vec<String>, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. @@ -69,7 +63,6 @@ impl Debug for AuthorizationCodeCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("AuthorizationCodeCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -88,7 +81,6 @@ impl AuthorizationCodeCredential { authorization_code: None, refresh_token: None, client_secret: "".to_string(), - scope: vec![], code_verifier: None, serializer: Default::default(), token_cache, @@ -206,44 +198,43 @@ impl TokenCache for AuthorizationCodeCredential { } impl AuthorizationCodeCredential { - pub fn new<T: AsRef<str>, U: IntoUrl>( - tenant_id: T, - client_id: T, - client_secret: T, - authorization_code: T, + pub fn new( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, + authorization_code: impl AsRef<str>, ) -> IdentityResult<AuthorizationCodeCredential> { Ok(AuthorizationCodeCredential { - app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), + app_config: AppConfig::builder(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), token_cache: Default::default(), }) } - pub fn new_with_redirect_uri<T: AsRef<str>, U: IntoUrl>( - tenant_id: T, - client_id: T, - client_secret: T, - authorization_code: T, - redirect_uri: U, + pub fn new_with_redirect_uri( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, + authorization_code: impl AsRef<str>, + redirect_uri: impl IntoUrl, ) -> IdentityResult<AuthorizationCodeCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; Ok(AuthorizationCodeCredential { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Some(tenant_id.as_ref().to_owned()), - Some(redirect_uri), - ), + app_config: AppConfigBuilder::new(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .redirect_uri(redirect_uri) + .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), token_cache: Default::default(), @@ -282,11 +273,10 @@ impl AuthorizationCodeCredentialBuilder { ) -> AuthorizationCodeCredentialBuilder { Self { credential: AuthorizationCodeCredential { - app_config: AppConfig::new_with_client_id(client_id.as_ref()), + app_config: AppConfig::new(client_id.as_ref()), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), token_cache: Default::default(), @@ -304,7 +294,6 @@ impl AuthorizationCodeCredentialBuilder { authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: String::new(), - scope: vec![], code_verifier: None, serializer: OAuthSerializer::new(), token_cache: Default::default(), @@ -375,7 +364,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { self.serializer .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) - .extend_scopes(self.scope.clone()); + .set_scope(self.app_config.scope.clone()); let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index abc340fa..735c8417 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -7,7 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; use crate::auth::{OAuthParameter, OAuthSerializer}; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; @@ -39,17 +39,6 @@ credential_builder!( #[derive(Clone)] pub struct ClientAssertionCredential { pub(crate) app_config: AppConfig, - /// The value passed for the scope parameter in this request should be the resource identifier - /// (application ID URI) of the resource you want, affixed with the .default suffix. - /// All scopes included must be for a single resource. Including scopes for multiple - /// resources will result in an error. - /// - /// For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// This value tells the Microsoft identity platform that of all the direct application - /// permissions you have configured for your app, the endpoint should issue a token for the - /// ones associated with the resource you want to use. To learn more about the /.default scope, - /// see the [consent documentation](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#the-default-scope). - pub(crate) scope: Vec<String>, /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. /// This is automatically set by the SDK. pub(crate) client_assertion_type: String, @@ -70,8 +59,10 @@ impl ClientAssertionCredential { client_id: impl AsRef<str>, ) -> ClientAssertionCredential { ClientAssertionCredential { - app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), - scope: vec!["https://graph.microsoft.com/.default".into()], + app_config: AppConfig::builder(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: assertion.as_ref().to_string(), serializer: Default::default(), @@ -84,7 +75,6 @@ impl Debug for ClientAssertionCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientAssertionCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -144,12 +134,9 @@ impl ClientAssertionCredentialBuilder { ) -> ClientAssertionCredentialBuilder { ClientAssertionCredentialBuilder { credential: ClientAssertionCredential { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - None, - ), - scope: vec!["https://graph.microsoft.com/.default".into()], + app_config: AppConfig::builder(client_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), client_assertion: signed_assertion.as_ref().to_owned(), serializer: Default::default(), @@ -160,12 +147,14 @@ impl ClientAssertionCredentialBuilder { pub(crate) fn new_with_signed_assertion( signed_assertion: String, - app_config: AppConfig, + mut app_config: AppConfig, ) -> ClientAssertionCredentialBuilder { + app_config + .scope + .insert("https://graph.microsoft.com/.default".to_string()); ClientAssertionCredentialBuilder { credential: ClientAssertionCredential { app_config, - scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), client_assertion: signed_assertion, serializer: Default::default(), @@ -199,14 +188,9 @@ impl TokenCredentialExecutor for ClientAssertionCredential { self.serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) - .client_assertion_type(self.client_assertion_type.as_str()); - - if self.scope.is_empty() { - self.serializer - .add_scope("https://graph.microsoft.com/.default"); - } - - self.serializer.grant_type("client_credentials"); + .client_assertion_type(self.client_assertion_type.as_str()) + .set_scope(self.app_config.scope.clone()) + .grant_type("client_credentials"); self.serializer.as_credential_map( vec![OAuthParameter::Scope], diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 7afff746..e5418ebe 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -31,19 +31,6 @@ macro_rules! credential_builder_base { self } - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( - &mut self, - scope: I, - ) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_default_scope(&mut self) -> &mut Self { - self.credential.scope = vec!["https://graph.microsoft.com/.default".to_string()]; - self - } - /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { @@ -93,6 +80,15 @@ macro_rules! credential_builder_base { .extend(header_parameters); self } + + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>( + &mut self, + scope: I, + ) -> &mut Self { + self.credential.app_config.scope = + scope.into_iter().map(|s| s.to_string()).collect(); + self + } } }; } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index f3a1f511..ca92ca26 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -6,7 +6,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -45,17 +45,6 @@ credential_builder!( #[allow(dead_code)] pub struct ClientCertificateCredential { pub(crate) app_config: AppConfig, - /// The value passed for the scope parameter in this request should be the resource identifier - /// (application ID URI) of the resource you want, affixed with the .default suffix. - /// All scopes included must be for a single resource. Including scopes for multiple - /// resources will result in an error. - /// - /// For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// This value tells the Microsoft identity platform that of all the direct application - /// permissions you have configured for your app, the endpoint should issue a token for the - /// ones associated with the resource you want to use. To learn more about the /.default scope, - /// see the [consent documentation](https://learn.microsoft.com/en-us/entra/identity-platform/permissions-consent-overview#the-default-scope). - pub(crate) scope: Vec<String>, /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. /// This value is automatically set by the SDK. pub(crate) client_assertion_type: String, @@ -75,8 +64,9 @@ pub struct ClientCertificateCredential { impl ClientCertificateCredential { pub fn new<T: AsRef<str>>(client_id: T, client_assertion: T) -> ClientCertificateCredential { ClientCertificateCredential { - app_config: AppConfig::new_with_client_id(client_id), - scope: vec!["https://graph.microsoft.com/.default".into()], + app_config: AppConfig::builder(client_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), serializer: Default::default(), @@ -109,7 +99,6 @@ impl Debug for ClientCertificateCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientCertificateCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -177,12 +166,8 @@ impl TokenCredentialExecutor for ClientCertificateCredential { .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) - .grant_type("client_credentials"); - - if self.scope.is_empty() { - self.serializer - .add_scope("https://graph.microsoft.com/.default"); - } + .grant_type("client_credentials") + .set_scope(self.app_config.scope.clone()); self.serializer.as_credential_map( vec![OAuthParameter::Scope], @@ -221,8 +206,9 @@ impl ClientCertificateCredentialBuilder { fn new<T: AsRef<str>>(client_id: T) -> ClientCertificateCredentialBuilder { ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { - app_config: AppConfig::new_with_client_id(client_id.as_ref()), - scope: vec!["https://graph.microsoft.com/.default".into()], + app_config: AppConfig::builder(client_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: Default::default(), serializer: OAuthSerializer::new(), @@ -234,12 +220,14 @@ impl ClientCertificateCredentialBuilder { #[cfg(feature = "openssl")] pub(crate) fn new_with_certificate( x509: &X509Certificate, - app_config: AppConfig, + mut app_config: AppConfig, ) -> anyhow::Result<ClientCertificateCredentialBuilder> { + app_config + .scope + .insert("https://graph.microsoft.com/.default".into()); let mut credential_builder = ClientCertificateCredentialBuilder { credential: ClientCertificateCredential { app_config, - scope: vec!["https://graph.microsoft.com/.default".into()], client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: Default::default(), serializer: OAuthSerializer::new(), diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 42e546df..90bba47e 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -24,11 +24,9 @@ impl ClientCredentialsAuthorizationUrlParameters { let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; Ok(ClientCredentialsAuthorizationUrlParameters { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - Some(redirect_uri), - ), + app_config: AppConfig::builder(client_id.as_ref()) + .redirect_uri(redirect_uri) + .build(), state: None, }) } @@ -80,10 +78,10 @@ pub struct ClientCredentialsAuthorizationUrlParameterBuilder { } impl ClientCredentialsAuthorizationUrlParameterBuilder { - pub fn new<T: AsRef<str>>(client_id: T) -> Self { + pub fn new(client_id: impl AsRef<str>) -> Self { Self { parameters: ClientCredentialsAuthorizationUrlParameters { - app_config: AppConfig::new_with_client_id(client_id), + app_config: AppConfig::new(client_id.as_ref()), state: None, }, } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index ced21e58..e150f2ef 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -6,7 +6,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -44,11 +44,6 @@ pub struct ClientSecretCredential { /// specification. The Basic auth pattern of instead providing credentials in the Authorization /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, - /// The value passed for the scope parameter in this request should be the resource - /// identifier (application ID URI) of the resource you want, affixed with the .default - /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// Default is https://graph.microsoft.com/.default. - pub(crate) scope: Vec<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -57,31 +52,36 @@ impl Debug for ClientSecretCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientSecretCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } impl ClientSecretCredential { - pub fn new<T: AsRef<str>>(client_id: T, client_secret: T) -> ClientSecretCredential { + pub fn new( + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, + ) -> ClientSecretCredential { ClientSecretCredential { - app_config: AppConfig::new_with_client_id(client_id), + app_config: AppConfig::builder(client_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_secret: client_secret.as_ref().to_owned(), - scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), token_cache: InMemoryCacheStore::new(), } } - pub fn new_with_tenant<T: AsRef<str>>( - tenant_id: T, - client_id: T, - client_secret: T, + pub fn new_with_tenant( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, ) -> ClientSecretCredential { ClientSecretCredential { - app_config: AppConfig::new_with_tenant_and_client_id(tenant_id, client_id), + app_config: AppConfig::builder(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), client_secret: client_secret.as_ref().to_owned(), - scope: vec!["https://graph.microsoft.com/.default".into()], serializer: OAuthSerializer::new(), token_cache: InMemoryCacheStore::new(), } @@ -156,14 +156,8 @@ impl TokenCredentialExecutor for ClientSecretCredential { self.serializer .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) - .grant_type("client_credentials"); - - if self.scope.is_empty() { - self.serializer - .extend_scopes(vec!["https://graph.microsoft.com/.default".to_owned()]); - } else { - self.serializer.extend_scopes(&self.scope); - } + .grant_type("client_credentials") + .set_scope(self.app_config.scope.clone()); // Don't include ClientId and Client Secret in the fields for form url encode because // Client Id and Client Secret are already included as basic auth. @@ -201,7 +195,7 @@ pub struct ClientSecretCredentialBuilder { } impl ClientSecretCredentialBuilder { - pub fn new<T: AsRef<str>>(client_id: T, client_secret: T) -> Self { + pub fn new(client_id: impl AsRef<str>, client_secret: impl AsRef<str>) -> Self { ClientSecretCredentialBuilder { credential: ClientSecretCredential::new(client_id, client_secret), } @@ -209,13 +203,15 @@ impl ClientSecretCredentialBuilder { pub(crate) fn new_with_client_secret( client_secret: impl AsRef<str>, - app_config: AppConfig, + mut app_config: AppConfig, ) -> ClientSecretCredentialBuilder { + app_config + .scope + .insert("https://graph.microsoft.com/.default".into()); Self { credential: ClientSecretCredential { app_config, client_secret: client_secret.as_ref().to_string(), - scope: vec!["https://graph.microsoft.com/.default".into()], serializer: Default::default(), token_cache: InMemoryCacheStore::new(), }, diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 47d96d1d..700a0be2 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -47,24 +47,6 @@ impl ConfidentialClientApplication<()> { } } -impl ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { - pub fn parameter_builder( - credential: AuthCodeAuthorizationUrlParameters, - ) -> ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { - ConfidentialClientApplication { credential } - } - - pub async fn interactive_auth( - &self, - options: Option<WebViewOptions>, - ) -> anyhow::Result<AuthorizationQueryResponse> { - let result = self - .credential - .interactive_webview_authentication(options)?; - Ok(result) - } -} - impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> ConfidentialClientApplication<Credential> { @@ -187,62 +169,6 @@ impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> } } -impl From<AuthCodeAuthorizationUrlParameters> - for ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> -{ - fn from(value: AuthCodeAuthorizationUrlParameters) -> Self { - ConfidentialClientApplication::parameter_builder(value) - } -} - -impl ConfidentialClientApplication<AuthCodeAuthorizationUrlParameters> { - pub fn interactive_webview_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> anyhow::Result<AuthorizationQueryResponse> { - let receiver = self - .credential - .interactive_authentication(interactive_web_view_options)?; - let mut iter = receiver.try_iter(); - let mut next = iter.next(); - while next.is_none() { - next = iter.next(); - } - - return match next { - None => Err(anyhow::anyhow!("Unknown")), - Some(auth_event) => match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; - - let response_query: AuthorizationQueryResponse = - serde_urlencoded::from_str(query)?; - Ok(response_query) - } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), - WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) - } - WindowCloseReason::TimedOut { - start: _, - requested_resume: _, - } => Err(anyhow::anyhow!("TimedOut")), - } - } - }, - }; - } -} - #[cfg(test)] mod test { use crate::identity::Authority; diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 6b07d9a4..c9d25363 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -5,7 +5,7 @@ use std::ops::Add; use std::str::FromStr; use std::time::Duration; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; use url::Url; use uuid::Uuid; @@ -15,7 +15,7 @@ use graph_core::http::{ }; use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, - IdentityResult, AF, + IdentityResult, WebViewExecutionError, WebViewResult, AF, }; use crate::auth::{OAuthParameter, OAuthSerializer}; @@ -52,27 +52,20 @@ pub struct DeviceCodeCredential { /// A device_code is a long string used to verify the session between the client and the authorization server. /// The client uses this parameter to request the access token from the authorization server. pub(crate) device_code: Option<String>, - /// A space-separated list of scopes. The scopes must all be from a single resource, - /// along with OIDC scopes (profile, openid, email). For more information, see Permissions - /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension - /// to the authorization code flow, intended to allow apps to declare the resource they want - /// the token for during token redemption. - pub(crate) scope: Vec<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } impl DeviceCodeCredential { - pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( - client_id: T, - device_code: T, + pub fn new<U: ToString, I: IntoIterator<Item = U>>( + client_id: impl AsRef<str>, + device_code: impl AsRef<str>, scope: I, ) -> DeviceCodeCredential { DeviceCodeCredential { - app_config: AppConfig::new_with_client_id(client_id), + app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(), refresh_token: None, device_code: Some(device_code.as_ref().to_owned()), - scope: scope.into_iter().map(|s| s.to_string()).collect(), serializer: Default::default(), token_cache: Default::default(), } @@ -124,7 +117,6 @@ impl Debug for DeviceCodeCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("DeviceCodeCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -232,7 +224,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { self.serializer .client_id(client_id.as_str()) - .extend_scopes(self.scope.clone()); + .set_scope(self.app_config.scope.clone()); if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { @@ -307,21 +299,20 @@ pub struct DeviceCodeCredentialBuilder { } impl DeviceCodeCredentialBuilder { - fn new<T: AsRef<str>>(client_id: T) -> DeviceCodeCredentialBuilder { + fn new(client_id: impl AsRef<str>) -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { credential: DeviceCodeCredential { - app_config: AppConfig::new_with_client_id(client_id.as_ref()), + app_config: AppConfig::new(client_id.as_ref()), refresh_token: None, device_code: None, - scope: vec![], serializer: Default::default(), token_cache: Default::default(), }, } } - pub(crate) fn new_with_device_code<T: AsRef<str>>( - device_code: T, + pub(crate) fn new_with_device_code( + device_code: impl AsRef<str>, app_config: AppConfig, ) -> DeviceCodeCredentialBuilder { DeviceCodeCredentialBuilder { @@ -329,7 +320,6 @@ impl DeviceCodeCredentialBuilder { app_config, refresh_token: None, device_code: Some(device_code.as_ref().to_owned()), - scope: vec![], serializer: Default::default(), token_cache: Default::default(), }, @@ -377,7 +367,6 @@ impl DeviceCodePollingExecutor { app_config, refresh_token: None, device_code: None, - scope: vec![], serializer: Default::default(), token_cache: Default::default(), }, @@ -385,14 +374,14 @@ impl DeviceCodePollingExecutor { } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } pub fn interactive_webview_authentication( &self, options: Option<WebViewOptions>, - ) -> anyhow::Result<AuthorizationQueryResponse> { + ) -> WebViewResult<AuthorizationQueryResponse> { let receiver = self.credential.interactive_authentication(options)?; let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -402,17 +391,15 @@ impl DeviceCodePollingExecutor { } return match next { - None => Err(anyhow::anyhow!("Unknown")), + None => unreachable!(), Some(auth_event) => match auth_event { InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(anyhow::anyhow!("Invalid Redirect Uri - {reason}")) + Err(WebViewExecutionError::InvalidRedirectUri(reason)) } InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let url_str = uri.as_str(); - let query = uri.query().or(uri.fragment()).ok_or(AF::msg_err( - "query | fragment", - &format!("No query or fragment returned on redirect uri: {url_str}"), - ))?; + let query = uri.query().or(uri.fragment()).ok_or( + WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), + )?; let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; @@ -420,14 +407,16 @@ impl DeviceCodePollingExecutor { } InteractiveAuthEvent::ClosingWindow(window_close_reason) => { match window_close_reason { - WindowCloseReason::CloseRequested => Err(anyhow::anyhow!("CloseRequested")), + WindowCloseReason::CloseRequested => { + Err(WebViewExecutionError::WindowClosedRequested) + } WindowCloseReason::InvalidWindowNavigation => { - Err(anyhow::anyhow!("InvalidWindowNavigation")) + Err(WebViewExecutionError::WindowClosedOnInvalidNavigation) } WindowCloseReason::TimedOut { start: _, requested_resume: _, - } => Err(anyhow::anyhow!("TimedOut")), + } => Err(WebViewExecutionError::WindowClosedOnTimeoutReached), } } }, @@ -595,17 +584,18 @@ pub(crate) mod web_view_authenticator { use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, }; - use graph_error::IdentityResult; + use graph_error::WebViewResult; impl InteractiveAuthenticator for DeviceCodeCredential { fn interactive_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, - ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { + ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { let uri = self .app_config .azure_cloud_instance - .auth_uri(&self.app_config.authority)?; + .auth_uri(&self.app_config.authority) + .expect("Internal Error Please Report"); let redirect_uri = self.app_config.redirect_uri.clone().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index b9d9ae30..3311a5bf 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -31,13 +31,6 @@ pub struct ImplicitCredential { /// also contain code in place of token to provide an authorization code, for use in the /// authorization code flow. This id_token+code response is sometimes called the hybrid flow. pub(crate) response_type: Vec<ResponseType>, - /// Required - /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the - /// scope openid, which translates to the "Sign you in" permission in the consent UI. - /// Optionally you may also want to include the email and profile scopes for gaining access - /// to additional user data. You may also include other scopes in this request for requesting - /// consent to various resources, if an access token is requested. - pub(crate) scope: Vec<String>, /// Optional /// Specifies the method that should be used to send the resulting token back to your app. /// Defaults to query for just an access token, but fragment if the request includes an id_token. @@ -83,26 +76,24 @@ pub struct ImplicitCredential { } impl ImplicitCredential { - pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( - client_id: T, - nonce: T, + pub fn new<U: ToString, I: IntoIterator<Item = U>>( + client_id: impl AsRef<str>, scope: I, - ) -> ImplicitCredential { - ImplicitCredential { - app_config: AppConfig::new_with_client_id(client_id), + ) -> IdentityResult<ImplicitCredential> { + Ok(ImplicitCredential { + app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(), response_type: vec![ResponseType::Token], - scope: scope.into_iter().map(|s| s.to_string()).collect(), response_mode: ResponseMode::Query, state: None, - nonce: nonce.as_ref().to_owned(), + nonce: secure_random_32()?, prompt: None, login_hint: None, domain_hint: None, - } + }) } - pub fn builder() -> ImplicitCredentialBuilder { - ImplicitCredentialBuilder::new() + pub fn builder(client_id: impl AsRef<str>) -> IdentityResult<ImplicitCredentialBuilder> { + ImplicitCredentialBuilder::new(client_id) } pub fn url(&self) -> IdentityResult<Url> { @@ -123,7 +114,7 @@ impl ImplicitCredential { serializer .client_id(client_id.as_str()) .nonce(self.nonce.as_str()) - .extend_scopes(self.scope.clone()) + .set_scope(self.app_config.scope.clone()) .authority(azure_cloud_instance, &self.app_config.authority); let response_types: Vec<String> = @@ -157,7 +148,7 @@ impl ImplicitCredential { } // https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc - if self.scope.is_empty() { + if self.app_config.scope.is_empty() { if self.response_type.contains(&ResponseType::IdToken) { serializer.add_scope("openid"); } else { @@ -220,27 +211,20 @@ pub struct ImplicitCredentialBuilder { credential: ImplicitCredential, } -impl Default for ImplicitCredentialBuilder { - fn default() -> Self { - Self::new() - } -} - impl ImplicitCredentialBuilder { - pub fn new() -> ImplicitCredentialBuilder { - ImplicitCredentialBuilder { + pub fn new(client_id: impl AsRef<str>) -> IdentityResult<ImplicitCredentialBuilder> { + Ok(ImplicitCredentialBuilder { credential: ImplicitCredential { - app_config: Default::default(), + app_config: AppConfig::new(client_id.as_ref()), response_type: vec![ResponseType::Code], - scope: vec![], response_mode: ResponseMode::Query, state: None, - nonce: String::new(), + nonce: secure_random_32()?, prompt: None, login_hint: None, domain_hint: None, }, - } + }) } pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { @@ -344,9 +328,9 @@ mod test { #[test] fn serialize_uri() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://localhost/myapp") .unwrap() @@ -364,9 +348,9 @@ mod test { #[test] fn set_open_id_fragment() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https://localhost:8080/myapp") @@ -384,9 +368,9 @@ mod test { #[test] fn set_open_id_fragment2() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https://localhost:8080/myapp") .unwrap() @@ -403,9 +387,9 @@ mod test { #[test] fn response_type_join() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() @@ -422,9 +406,9 @@ mod test { #[test] fn response_type_join_string() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::StringSet( vec!["id_token".to_owned(), "token".to_owned()] .into_iter() @@ -445,9 +429,9 @@ mod test { #[test] fn response_type_into_iter() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::IdToken) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() @@ -464,9 +448,9 @@ mod test { #[test] fn response_type_into_iter2() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() @@ -484,9 +468,9 @@ mod test { #[test] #[should_panic] fn missing_scope_panic() { - let mut authorizer = ImplicitCredential::builder(); + let mut authorizer = + ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer - .with_client_id("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://example.com/myapp") .unwrap() @@ -498,14 +482,13 @@ mod test { #[test] fn generate_nonce() { - let url = ImplicitCredential::builder() + let url = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") + .unwrap() .with_redirect_uri("http://localhost:8080") .unwrap() .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_nonce_generated() - .unwrap() .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 8d96a246..fee07c07 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -15,6 +15,8 @@ use crate::identity::{ AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; +const RESPONSE_TYPES_SUPPORTED: &[&str] = &["code", "id_token", "code id_token", "id_token token"]; + /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional /// authentication protocol. You can use OIDC to enable single sign-on (SSO) between your /// OAuth-enabled applications by using a security token called an ID token. @@ -65,11 +67,6 @@ pub struct OpenIdAuthorizationUrlParameters { /// information about the user's state in the app before the authentication request occurred, /// such as the page or view the user was on. pub(crate) state: Option<String>, - /// Required - the openid scope is already included. - /// A space-separated list of scopes. For OpenID Connect, it must include the scope openid, - /// which translates to the Sign you in permission in the consent UI. You might also include - /// other scopes in this request for requesting consent. - pub(crate) scope: BTreeSet<String>, /// Optional - /// Indicates the type of user interaction that is required. The only valid values at /// this time are login, none, consent, and select_account. @@ -103,14 +100,12 @@ pub struct OpenIdAuthorizationUrlParameters { /// this parameter during re-authentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, - response_types_supported: Vec<String>, } impl Debug for OpenIdAuthorizationUrlParameters { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("AuthCodeAuthorizationUrlParameters") + f.debug_struct("OpenIdAuthorizationUrlParameters") .field("app_config", &self.app_config) - .field("scope", &self.scope) .field("response_type", &self.response_type) .field("response_mode", &self.response_mode) .field("prompt", &self.prompt) @@ -123,42 +118,33 @@ impl OpenIdAuthorizationUrlParameters { redirect_uri: IU, scope: I, ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { - let mut tree_set_scope = BTreeSet::new(); - tree_set_scope.insert("openid".to_owned()); - tree_set_scope.extend(scope.into_iter().map(|s| s.to_string())); + let mut scope_set = BTreeSet::new(); + scope_set.insert("openid".to_owned()); + scope_set.extend(scope.into_iter().map(|s| s.to_string())); let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let mut app_config = AppConfig::new_with_client_id(client_id); - app_config.redirect_uri = Some(redirect_uri.into_url().or(redirect_uri_result)?); let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::IdToken); Ok(OpenIdAuthorizationUrlParameters { - app_config, + app_config: AppConfig::builder(client_id.as_ref()) + .scope(scope_set) + .redirect_uri(redirect_uri.into_url().or(redirect_uri_result)?) + .build(), response_type, response_mode: None, nonce: secure_random_32()?, state: None, - scope: tree_set_scope, - prompt: BTreeSet::new(), + prompt: Default::default(), domain_hint: None, login_hint: None, - response_types_supported: vec![ - "code".into(), - "id_token".into(), - "code id_token".into(), - "id_token token".into(), - ], }) } fn new_with_app_config( app_config: AppConfig, ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { - let mut scope = BTreeSet::new(); - scope.insert("openid".to_owned()); - let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::IdToken); @@ -168,16 +154,9 @@ impl OpenIdAuthorizationUrlParameters { response_mode: None, nonce: secure_random_32()?, state: None, - scope, prompt: Default::default(), domain_hint: None, login_hint: None, - response_types_supported: vec![ - "code".into(), - "id_token".into(), - "code id_token".into(), - "id_token token".into(), - ], }) } @@ -226,7 +205,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { return AuthorizationFailure::result("client_id"); } - if self.scope.is_empty() { + if self.app_config.scope.is_empty() { return AuthorizationFailure::result("scope"); } @@ -239,7 +218,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer .client_id(client_id.as_str()) - .extend_scopes(self.scope.clone()) + .set_scope(self.app_config.scope.clone()) .nonce(self.nonce.as_str()) .authority(azure_cloud_instance, &self.app_config.authority); @@ -247,12 +226,12 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer.response_type("code"); } else { let response_types = self.response_type.as_query(); - if !self.response_types_supported.contains(&response_types) { + if !RESPONSE_TYPES_SUPPORTED.contains(&response_types.as_str()) { return AuthorizationFailure::msg_result( "response_type", format!( "response_type is not supported - supported response types are: {}", - self.response_types_supported + RESPONSE_TYPES_SUPPORTED .iter() .map(|s| format!("`{}`", s)) .collect::<Vec<String>>() @@ -324,7 +303,9 @@ impl OpenIdAuthorizationUrlParameterBuilder { ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { Ok(OpenIdAuthorizationUrlParameterBuilder { parameters: OpenIdAuthorizationUrlParameters::new_with_app_config( - AppConfig::new_with_client_id(client_id), + AppConfig::builder(client_id.as_ref()) + .scope(vec!["openid"]) + .build(), )?, }) } @@ -332,9 +313,6 @@ impl OpenIdAuthorizationUrlParameterBuilder { pub(crate) fn new_with_app_config( app_config: AppConfig, ) -> OpenIdAuthorizationUrlParameterBuilder { - let mut scope = BTreeSet::new(); - scope.insert("openid".to_owned()); - OpenIdAuthorizationUrlParameterBuilder { parameters: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config) .expect("ring::crypto::Unspecified"), @@ -424,20 +402,23 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// Takes an iterator of scopes to use in the request. /// Replaces current scopes if any were added previously. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.parameters.scope = scope.into_iter().map(|s| s.to_string()).collect(); + if self.parameters.app_config.scope.contains("offline_access") { + self.parameters.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.with_offline_access(); + } else { + self.parameters.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + } self } - /// Automatically adds `profile` and `email` to the scope parameter. - /// The `openid` scope is already included in the request. - /// - /// If you need a refresh token then include `offline_access` as a scope. - /// The `offline_access` scope is not included here. - pub fn with_default_scope(&mut self) -> anyhow::Result<&mut Self> { + /// Adds the `offline_access` scope parameter which tells the authorization server + /// to include a refresh token in the response. + pub fn with_offline_access(&mut self) -> &mut Self { self.parameters + .app_config .scope - .extend(vec!["profile".to_owned(), "email".to_owned()]); - Ok(self) + .extend(vec!["offline_access".to_owned()]); + self } /// Indicates the type of user interaction that is required. Valid values are login, none, @@ -490,7 +471,7 @@ mod test { #[test] #[should_panic] - fn unsupported_response_type() { + fn code_token_unsupported_response_type() { let _ = OpenIdAuthorizationUrlParameters::builder("client_id") .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) @@ -498,4 +479,15 @@ mod test { .url() .unwrap(); } + + #[test] + #[should_panic] + fn id_token_token_unsupported_response_type() { + let _ = OpenIdAuthorizationUrlParameters::builder("client_id") + .unwrap() + .with_response_type([ResponseType::Token]) + .with_scope(["scope"]) + .url() + .unwrap(); + } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 9a0f8629..9e568ae9 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use async_trait::async_trait; -use graph_core::cache::{InMemoryCacheStore, TokenCache}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use url::Url; @@ -11,7 +11,7 @@ use uuid::Uuid; use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::identity::credentials::app_config::AppConfig; +use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, @@ -50,14 +50,6 @@ pub struct OpenIdCredential { /// specification. The Basic auth pattern of instead providing credentials in the Authorization /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, - /// The same redirect_uri value that was used to acquire the authorization_code. - // pub(crate) redirect_uri: Url, - /// A space-separated list of scopes. The scopes must all be from a single resource, - /// along with OIDC scopes (profile, openid, email). For more information, see Permissions - /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension - /// to the authorization code flow, intended to allow apps to declare the resource they want - /// the token for during token redemption. - pub(crate) scope: Vec<String>, /// The same code_verifier that was used to obtain the authorization_code. /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. @@ -72,7 +64,6 @@ impl Debug for OpenIdCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("OpenIdCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -86,15 +77,13 @@ impl OpenIdCredential { ) -> IdentityResult<OpenIdCredential> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); Ok(OpenIdCredential { - app_config: AppConfig::new_init( - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(), - Option::<String>::None, - Some(redirect_uri.into_url().or(redirect_uri_result)?), - ), + app_config: AppConfigBuilder::new(client_id.as_ref()) + .redirect_uri(redirect_uri.into_url().or(redirect_uri_result)?) + .scope(vec!["openid"]) + .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - scope: vec!["openid".to_owned()], code_verifier: None, pkce: None, serializer: Default::default(), @@ -114,8 +103,8 @@ impl OpenIdCredential { pub fn authorization_url_builder( client_id: impl AsRef<str>, ) -> OpenIdAuthorizationUrlParameterBuilder { - OpenIdAuthorizationUrlParameterBuilder::new_with_app_config(AppConfig::new_with_client_id( - client_id, + OpenIdAuthorizationUrlParameterBuilder::new_with_app_config(AppConfig::new( + client_id.as_ref(), )) } @@ -248,7 +237,7 @@ impl TokenCredentialExecutor for OpenIdCredential { self.serializer .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) - .extend_scopes(self.scope.clone()); + .set_scope(self.app_config.scope.clone()); if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { @@ -284,6 +273,9 @@ impl TokenCredentialExecutor for OpenIdCredential { .authorization_code(authorization_code.as_ref()) .grant_type("authorization_code"); + // Authorization codes can only be used once. Remove it from the configuration. + self.authorization_code = None; + if let Some(code_verifier) = self.code_verifier.as_ref() { self.serializer.code_verifier(code_verifier.as_str()); } @@ -343,11 +335,11 @@ impl OpenIdCredentialBuilder { fn new(client_id: impl TryInto<Uuid>) -> OpenIdCredentialBuilder { Self { credential: OpenIdCredential { - app_config: AppConfig::new_init( - client_id.try_into().unwrap_or_default(), - Option::<String>::None, - Some(Url::parse("http://localhost").expect("Internal Error - please report")), - ), + app_config: AppConfig::builder(client_id) + .redirect_uri( + Url::parse("http://localhost").expect("Internal Error - please report"), + ) + .build(), authorization_code: None, refresh_token: None, client_secret: String::new(), @@ -444,10 +436,7 @@ impl OpenIdCredentialBuilder { impl From<OpenIdAuthorizationUrlParameters> for OpenIdCredentialBuilder { fn from(value: OpenIdAuthorizationUrlParameters) -> Self { - let mut builder = OpenIdCredentialBuilder::new_with_app_config(value.app_config); - builder.with_scope(value.scope); - - builder + OpenIdCredentialBuilder::new_with_app_config(value.app_config) } } diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index d65210ea..9253d1fd 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -12,6 +12,10 @@ use uuid::Uuid; /// however this client implementation does not offer this use case. This is the /// same as all MSAL clients. /// https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.3 +/// +/// The Microsoft identity platform only supports the ROPC grant within Microsoft Entra tenants, +/// not personal accounts. This means that you must use a tenant-specific endpoint +/// (https://login.microsoftonline.com/{TenantId_or_Name}) or the organizations endpoint. #[derive(Clone)] pub struct ResourceOwnerPasswordCredential { pub(crate) app_config: AppConfig, @@ -21,11 +25,6 @@ pub struct ResourceOwnerPasswordCredential { /// Required /// The user's password. pub(crate) password: String, - /// The value passed for the scope parameter in this request should be the resource - /// identifier (application ID URI) of the resource you want, affixed with the .default - /// suffix. For the Microsoft Graph example, the value is https://graph.microsoft.com/.default. - /// Default is https://graph.microsoft.com/.default. - pub(crate) scope: Vec<String>, serializer: OAuthSerializer, } @@ -33,41 +32,38 @@ impl Debug for ResourceOwnerPasswordCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("ClientAssertionCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } impl ResourceOwnerPasswordCredential { - pub fn new<T: AsRef<str>>( - client_id: T, - username: T, - password: T, + pub fn new( + client_id: impl AsRef<str>, + username: impl AsRef<str>, + password: impl AsRef<str>, ) -> ResourceOwnerPasswordCredential { - let mut app_config = AppConfig::new_with_client_id(client_id.as_ref()); - app_config.authority = Authority::Organizations; ResourceOwnerPasswordCredential { - app_config, + app_config: AppConfig::builder(client_id.as_ref()) + .authority(Authority::Organizations) + .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - scope: vec![], serializer: Default::default(), } } - pub fn new_with_tenant<T: AsRef<str>>( - tenant_id: T, - client_id: T, - username: T, - password: T, + pub fn new_with_tenant( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + username: impl AsRef<str>, + password: impl AsRef<str>, ) -> ResourceOwnerPasswordCredential { - let mut app_config = AppConfig::new_with_tenant_and_client_id(tenant_id, client_id); - app_config.authority = Authority::Organizations; ResourceOwnerPasswordCredential { - app_config, + app_config: AppConfig::builder(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - scope: vec![], serializer: Default::default(), } } @@ -96,7 +92,7 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { self.serializer .client_id(client_id.as_str()) .grant_type("password") - .extend_scopes(self.scope.iter()); + .set_scope(self.app_config.scope.clone()); self.serializer.as_credential_map( vec![OAuthParameter::Scope], @@ -127,21 +123,20 @@ pub struct ResourceOwnerPasswordCredentialBuilder { } impl ResourceOwnerPasswordCredentialBuilder { - fn new<T: AsRef<str>>(client_id: T) -> ResourceOwnerPasswordCredentialBuilder { + fn new(client_id: impl AsRef<str>) -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { - app_config: AppConfig::new_with_client_id(client_id.as_ref()), + app_config: AppConfig::new(client_id.as_ref()), username: String::new(), password: String::new(), - scope: vec![], serializer: Default::default(), }, } } - pub(crate) fn new_with_username_password<T: AsRef<str>>( - username: T, - password: T, + pub(crate) fn new_with_username_password( + username: impl AsRef<str>, + password: impl AsRef<str>, app_config: AppConfig, ) -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { @@ -149,7 +144,6 @@ impl ResourceOwnerPasswordCredentialBuilder { app_config, username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - scope: vec![], serializer: Default::default(), }, } @@ -179,6 +173,11 @@ impl ResourceOwnerPasswordCredentialBuilder { self } + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + /// The grant type isn't supported on the /common or /consumers authentication contexts. /// Use /organizations or a tenant ID instead. /// Authority defaults to /organizations if no tenant id or authority is given. @@ -204,12 +203,6 @@ impl ResourceOwnerPasswordCredentialBuilder { Ok(self) } - /// Defaults to "https://graph.microsoft.com/.default" - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.credential.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - pub fn build(&self) -> ResourceOwnerPasswordCredential { self.credential.clone() } diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 1a32de1c..aa7dafea 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -18,20 +18,11 @@ //! - [Client Credentials - Client Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) //! - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) //! -//! #### Microsoft OneDrive and SharePoint -//! -//! Can only be used with personal Microsoft accounts. Not recommended - use the Microsoft -//! Identity Platform if at all possible. -//! -//! - [Token Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#token-flow) -//! - [Code Flow](https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow) -//! -//! //! # Example ConfidentialClientApplication Authorization Code Flow //! ```rust //! use url::Url; //! use graph_error::IdentityResult; -//! use graph_oauth::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; +//! use graph_oauth::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! //! pub fn authorization_url(client_id: &str) -> IdentityResult<Url> { //! ConfidentialClientApplication::builder(client_id) diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index c96477ff..788a4569 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -1,5 +1,5 @@ use crate::web::WebViewOptions; -use graph_error::IdentityResult; +use graph_error::WebViewResult; use std::time::Instant; use url::Url; @@ -7,7 +7,7 @@ pub trait InteractiveAuthenticator { fn interactive_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, - ) -> IdentityResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; + ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; } #[derive(Clone, Debug)] diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index aef07e74..76fb39d6 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -2,6 +2,7 @@ use std::time::Duration; use url::Url; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use graph_error::{WebViewExecutionError, WebViewResult}; use wry::application::event_loop::EventLoopBuilder; use wry::application::platform::windows::EventLoopBuilderExtWindows; use wry::{ @@ -32,11 +33,11 @@ impl WebViewValidHosts { start_uri: Url, redirect_uris: Vec<Url>, ports: Vec<usize>, - ) -> anyhow::Result<WebViewValidHosts> { + ) -> WebViewResult<WebViewValidHosts> { if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { - return Err(anyhow::Error::msg( - "authorization url and redirect uri must have valid uri host", - )); + return Err(WebViewExecutionError::InvalidStartUri { + reason: "Authorization url and redirect uri must have valid uri hosts".to_owned(), + }); } let is_local_host = redirect_uris @@ -44,9 +45,9 @@ impl WebViewValidHosts { .any(|uri| uri.as_str().eq("http://localhost")); if is_local_host && ports.is_empty() { - return Err(anyhow::anyhow!( - "Redirect uri is http://localhost but not ports were specified".to_string() - )); + return Err(WebViewExecutionError::InvalidStartUri { + reason: "Redirect uri is http://localhost but not ports were specified".to_string(), + }); } Ok(WebViewValidHosts { diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index e8be0a2b..57404ab3 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -5,16 +5,29 @@ pub use wry::application::window::Theme; #[derive(Clone, Debug)] pub struct WebViewOptions { + /// Give the window a title. The default is "Sign In" pub window_title: String, - // Close window if navigation to a uri that does not match one of the - // given redirect uri's. + /// Close the webview window whenever there is a navigation by the webview or user + /// to a url that is not one of the redirect urls or the login url. + /// For instance, if this is considered a security issue and the user should + /// not be able to navigate to another url. + /// Either way, the url bar does not show regardless. pub close_window_on_invalid_uri_navigation: bool, + /// OS specific theme. Does not work on all operating systems. + /// See wry crate for more info. pub theme: Option<Theme>, /// Provide a list of ports to use for interactive authentication. /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. pub ports: Vec<usize>, + /// Add a timeout that will close the window and return an error + /// when that timeout is reached. For instance, if your app is waiting on the + /// user to log in and the user has not logged in after 20 minutes you may + /// want to assume the user is idle in some way and close out of the webview window. pub timeout: Instant, + /// The webview can store the cookies that were set after sign in so that on the next + /// sign in the user is automatically logged in through SSO. Or you can clear the browsing + /// data, cookies in this case, after sign in when the webview window closes. pub clear_browsing_data: bool, } @@ -23,16 +36,24 @@ impl WebViewOptions { WebViewOptions::default() } + /// Give the window a title. The default is "Sign In" pub fn with_window_title(mut self, window_title: impl ToString) -> Self { self.window_title = window_title.to_string(); self } + /// Close the webview window whenever there is a navigation by the webview or user + /// to a url that is not one of the redirect urls or the login url. + /// For instance, if this is considered a security issue and the user should + /// not be able to navigate to another url. + /// Either way, the url bar does not show regardless. pub fn with_close_window_on_invalid_navigation(mut self, close_window: bool) -> Self { self.close_window_on_invalid_uri_navigation = close_window; self } + /// OS specific theme. Does not work on all operating systems. + /// See wry crate for more info. pub fn with_theme(mut self, theme: Theme) -> Self { self.theme = Some(theme); self @@ -43,11 +64,18 @@ impl WebViewOptions { self } + /// Add a timeout that will close the window and return an error + /// when that timeout is reached. For instance, if your app is waiting on the + /// user to log in and the user has not logged in after 20 minutes you may + /// want to assume the user is idle in some way and close out of the webview window. pub fn with_timeout(mut self, instant: Instant) -> Self { self.timeout = instant; self } + /// The webview can store the cookies that were set after sign in so that on the next + /// sign in the user is automatically logged in through SSO. Or you can clear the browsing + /// data, cookies in this case, after sign in when the webview window closes. pub fn with_clear_browsing_data(mut self, clear_browsing_data: bool) -> Self { self.clear_browsing_data = clear_browsing_data; self diff --git a/src/lib.rs b/src/lib.rs index 735bdb54..ed6d0d33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,9 @@ //! Graph API. There may be some requests and/or API not yet included in this project but in general most of them are //! implemented. //! +//! For any APIs missing you can make a feature request on GitHub or you can create a PR +//! to add the APIs. Contributions welcome. +//! //! ## Feature requests or Bug reports. //! //! For bug reports please file an issue on [GitHub](https://github.com/sreeise/graph-rs-sdk) @@ -21,9 +24,6 @@ //! than that feel free to ask questions, provide tips to others, and talk about the project in general. //! //! ## Use -//! The client is async by default and it is recommended to use -//! tokio as the runtime. Tokio is what is used internally and what the project -//! is tested with. //! //! ```rust,ignore //! use graph_rs_sdk::*; @@ -149,7 +149,7 @@ //! //! - For more information and examples please see the repository on //! [GitHub](https://github.com/sreeise/graph-rs-sdk) -//! - If you run into issues related to graph-rs specifically please +//! - If you run into issues related to graph-rs-sdk specifically please //! file an issue on [GitHub](https://github.com/sreeise/graph-rs-sdk) //! //! # OAuth diff --git a/tests/reports_request.rs b/tests/reports_request.rs index 7bbcbc82..d086704f 100644 --- a/tests/reports_request.rs +++ b/tests/reports_request.rs @@ -42,6 +42,8 @@ async fn async_download_office_365_user_counts_reports_test() { } } +// TODO: Test Failing +/* #[tokio::test] async fn get_office_365_user_counts_reports_text() { if Environment::is_local() { @@ -64,3 +66,4 @@ async fn get_office_365_user_counts_reports_text() { } } } + */ diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 49eaaae1..d20a8ddd 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -83,7 +83,7 @@ fn upload_reqwest_body() { assert!(body["id"].as_str().is_some()); let item_id = body["id"].as_str().unwrap(); - thread::sleep(Duration::from_secs(2)); + thread::sleep(Duration::from_secs(4)); let response = get_file_content(id.as_str(), item_id, &client).unwrap(); assert!(response.status().is_success()); From 6666fc28698c718b69b30b2b82ce02de1fda3859 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 6 Nov 2023 07:33:49 -0500 Subject: [PATCH 057/118] Handle webview and polling in device code --- examples/interactive_authentication/README.md | 18 +- graph-error/src/lib.rs | 1 + graph-error/src/webview_error.rs | 42 ++- .../auth_code_authorization_url.rs | 2 +- ...authorization_code_assertion_credential.rs | 13 +- ...thorization_code_certificate_credential.rs | 12 +- .../credentials/device_code_credential.rs | 320 +++++++++++++++--- .../credentials/open_id_credential.rs | 10 +- graph-oauth/src/identity/device_code.rs | 41 ++- .../src/web/interactive_authenticator.rs | 2 +- graph-oauth/src/web/interactive_web_view.rs | 127 ++++++- graph-oauth/src/web/web_view_options.rs | 6 +- 12 files changed, 490 insertions(+), 104 deletions(-) diff --git a/examples/interactive_authentication/README.md b/examples/interactive_authentication/README.md index f382325b..87f6892f 100644 --- a/examples/interactive_authentication/README.md +++ b/examples/interactive_authentication/README.md @@ -3,7 +3,23 @@ Interactive Authentication uses a webview to perform sign in and handle the redirect uri making it easy for you to integrate the sdk into your application. -The sdk uses the wry crate internally to provide a webview. +Interactive Authentication uses a webview provided by the Wry crate https://github.com/tauri-apps/wry +See the wry documentation for platform specific installation. Linux and macOS require +installation of platform specific dependencies. These are not included by default. + +This example executes the Authorization Code OAuth flow and handles +sign in/redirect using WebView as well as authorization and token retrieval. + +The WebView window will load on the sign in page for Microsoft Graph +Log in with a user and upon redirect the will close the window automatically. +The credential_builder will store the authorization code returned on the +redirect url after logging in and then build a `ConfidentialClient<AuthorizationCodeCredential>` + +The `ConfidentialClient<AuthorizationCodeCredential>` handles authorization to get an access token +on the first request made using the Graph client. The token is stored in an in memory cache +and subsequent calls will use this token. If a refresh token is included, which you can get +by requesting the offline_access scope, then the confidential client will take care of refreshing +the token. ### Example diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index 40d3abe9..a936a4c1 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -22,3 +22,4 @@ pub type IdentityResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; pub type WebViewResult<T> = Result<T, WebViewExecutionError>; +pub type DeviceCodeWebViewResult<T> = Result<T, WebViewDeviceCodeExecutionError>; diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index b6d99010..d7187a70 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -1,4 +1,4 @@ -use crate::AuthorizationFailure; +use crate::{AuthExecutionError, AuthorizationFailure}; use url::ParseError; #[derive(Debug, thiserror::Error)] @@ -37,3 +37,43 @@ pub enum WebViewExecutionError { #[error("{0:#?}")] AuthorizationError(#[from] AuthorizationFailure), } + +#[derive(Debug, thiserror::Error)] +pub enum WebViewDeviceCodeExecutionError { + // Issues with the redirect uri such as specifying localhost + // but not providing a port in the WebViewOptions. + #[error("InvalidRedirectUri: {0:#?}")] + InvalidRedirectUri(String), + /// The user closed the webview window without logging in. + #[error("WindowClosedRequested")] + WindowClosedRequested, + /// The user navigated to a url that was not the login url + /// or a redirect url specified. Requires that WebViewOptions + /// has the enforcement of invalid navigation enabled. + #[error("WindowClosedOnInvalidNavigation")] + WindowClosedOnInvalidNavigation, + /// The webview exited because of a timeout defined in the WebViewOptions. + #[error("WindowClosedOnTimeoutReached")] + WindowClosedOnTimeoutReached, + /// The host or domain provided or set for login is invalid. + /// This could be an internal error and most likely will never happen. + #[error("InvalidStartUri: {reason:#?}")] + InvalidStartUri { reason: String }, + /// The webview was successfully redirected but the url did not + /// contain a query or fragment. The query or fragment of the url + /// is where the auth code would be returned to the app. + #[error("No query or fragment returned on redirect uri: {0:#?}")] + RedirectUriMissingQueryOrFragment(String), + /// Serde serialization error when attempting to serialize + /// the query or fragment of the url to a AuthorizationQueryResponse + #[error("{0:#?}")] + SerdeError(#[from] serde::de::value::Error), + /// Error from building out the parameters necessary for authorization + /// this most likely came from an invalid parameter or missing parameter + /// passed to the client used for building the url. + #[error("{0:#?}")] + AuthorizationError(#[from] AuthorizationFailure), + /// Error that happens calling the http request. + #[error("{0:#?}")] + AuthExecutionError(#[from] AuthExecutionError), +} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 9367f22c..31a21d94 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -208,7 +208,7 @@ impl AuthCodeAuthorizationUrlParameters { serde_urlencoded::from_str(query)?; Ok(response_query) } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { + InteractiveAuthEvent::WindowClosed(window_close_reason) => { match window_close_reason { WindowCloseReason::CloseRequested => { Err(WebViewExecutionError::WindowClosedRequested) diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 9ab23421..7a090749 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -49,13 +49,6 @@ pub struct AuthorizationCodeAssertionCredential { /// you registered as credentials for your application. Read about certificate credentials /// to learn how to register your certificate and the format of the assertion. pub(crate) client_assertion: String, - /// Required - /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the - /// scope openid, which translates to the "Sign you in" permission in the consent UI. - /// Optionally you may also want to include the email and profile scopes for gaining access - /// to additional user data. You may also include other scopes in this request for requesting - /// consent to various resources, if an access token is requested. - pub(crate) scope: Vec<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -64,7 +57,6 @@ impl Debug for AuthorizationCodeAssertionCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("AuthorizationCodeAssertionCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -92,7 +84,6 @@ impl AuthorizationCodeAssertionCredential { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - scope: vec![], serializer: OAuthSerializer::new(), token_cache: Default::default(), }) @@ -244,7 +235,7 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) - .extend_scopes(self.scope.clone()); + .set_scope(self.app_config.scope.clone()); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { self.serializer.redirect_uri(redirect_uri.as_str()); @@ -346,7 +337,6 @@ impl AuthorizationCodeAssertionCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), - scope: vec![], serializer: OAuthSerializer::new(), token_cache: Default::default(), }, @@ -366,7 +356,6 @@ impl AuthorizationCodeAssertionCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: assertion.as_ref().to_owned(), - scope: vec![], serializer: OAuthSerializer::new(), token_cache: Default::default(), }, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index f3edc865..79187a94 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -49,13 +49,6 @@ pub struct AuthorizationCodeCertificateCredential { /// you registered as credentials for your application. Read about certificate credentials /// to learn how to register your certificate and the format of the assertion. pub(crate) client_assertion: String, - /// Required - /// A space-separated list of scopes. For OpenID Connect (id_tokens), it must include the - /// scope openid, which translates to the "Sign you in" permission in the consent UI. - /// Optionally you may also want to include the email and profile scopes for gaining access - /// to additional user data. You may also include other scopes in this request for requesting - /// consent to various resources, if an access token is requested. - pub(crate) scope: Vec<String>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -64,7 +57,6 @@ impl Debug for AuthorizationCodeCertificateCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("AuthorizationCodeCertificateCredential") .field("app_config", &self.app_config) - .field("scope", &self.scope) .finish() } } @@ -92,7 +84,6 @@ impl AuthorizationCodeCertificateCredential { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - scope: vec![], serializer: OAuthSerializer::new(), token_cache: Default::default(), }) @@ -247,7 +238,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) - .extend_scopes(self.scope.clone()); + .set_scope(self.app_config.scope.clone()); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { self.serializer.redirect_uri(redirect_uri.as_str()); @@ -351,7 +342,6 @@ impl AuthorizationCodeCertificateCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), - scope: vec![], serializer: OAuthSerializer::new(), token_cache: Default::default(), }, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index c9d25363..026f7e65 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -2,7 +2,10 @@ use async_trait::async_trait; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::ops::Add; +use std::process::exit; use std::str::FromStr; +use std::sync::mpsc::Receiver; +use std::thread; use std::time::Duration; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; @@ -15,18 +18,18 @@ use graph_core::http::{ }; use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, - IdentityResult, WebViewExecutionError, WebViewResult, AF, + DeviceCodeWebViewResult, IdentityResult, WebViewExecutionError, WebViewResult, AF, }; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AuthorizationQueryResponse, AzureCloudInstance, DeviceCode, ForceTokenRefresh, - PollDeviceCodeType, PublicClientApplication, TokenCredentialExecutor, + PollDeviceCodeEvent, PublicClientApplication, TokenCredentialExecutor, }; -use crate::oauth::Token; -use crate::web::InteractiveAuthenticator; +use crate::oauth::{InteractiveDeviceCodeEvent, Token}; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use crate::web::{InteractiveAuthenticator, InteractiveWebView}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -210,7 +213,9 @@ impl TokenCredentialExecutor for DeviceCodeCredential { .authority_device_code(&azure_cloud_instance, &self.authority()); if self.device_code.is_none() && self.refresh_token.is_none() { - Ok(self.azure_cloud_instance().auth_uri(&self.authority())?) + Ok(self + .azure_cloud_instance() + .device_code_uri(&self.authority())?) } else { Ok(self.azure_cloud_instance().token_uri(&self.authority())?) } @@ -378,49 +383,252 @@ impl DeviceCodePollingExecutor { self } + pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + self.credential.app_config.tenant_id = Some(tenant_id.as_ref().to_owned()); + self + } + + pub fn interactive_webview_auth(&mut self, options: Option<WebViewOptions>) { + let executor_result = self.interactive_webview_authentication(options); + if let Ok(mut executor) = executor_result { + while let interactive_device_code_event = executor.recv().unwrap() { + match interactive_device_code_event { + InteractiveDeviceCodeEvent::BeginAuth { + response, + device_code, + } => { + println!("{:#?}", response); + println!("{:#?}", device_code); + } + InteractiveDeviceCodeEvent::FailedAuth { + response, + device_code, + } => { + println!("{:#?}", response); + println!("{:#?}", device_code); + } + InteractiveDeviceCodeEvent::PollDeviceCode { + poll_device_code_event, + response, + } => { + println!("{:#?}", response); + println!("{:#?}", poll_device_code_event); + } + InteractiveDeviceCodeEvent::InteractiveAuthEvent(auth_event) => { + match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(_) => {} + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + println!("{:#?}", uri); + } + InteractiveAuthEvent::WindowClosed(window_closed) => { + match window_closed { + WindowCloseReason::CloseRequested => { + println!("close requested"); + } + WindowCloseReason::InvalidWindowNavigation => { + println!("invalid navigation"); + } + WindowCloseReason::TimedOut { + requested_resume, + start, + } => { + println!("Timed Out"); + } + } + } + } + } + InteractiveDeviceCodeEvent::SuccessfulAuthEvent { + response, + public_application, + } => { + println!("{:#?}", response); + let json = response.json().unwrap(); + let token: Token = serde_json::from_value(json).unwrap(); + println!("{:#?}", token); + } + } + } + } else if let Err(err) = executor_result { + println!("{err:#?}"); + } + } + pub fn interactive_webview_authentication( - &self, + &mut self, options: Option<WebViewOptions>, - ) -> WebViewResult<AuthorizationQueryResponse> { - let receiver = self.credential.interactive_authentication(options)?; - let mut iter = receiver.try_iter(); - let mut next = iter.next(); + ) -> DeviceCodeWebViewResult<Receiver<InteractiveDeviceCodeEvent>> { + let (sender, receiver) = std::sync::mpsc::channel(); + + let mut credential = self.credential.clone(); + let response = credential.execute()?; + + let http_response = response.into_http_response()?; - while next.is_none() { - next = iter.next(); + if !http_response.status().is_success() { + sender + .send(InteractiveDeviceCodeEvent::FailedAuth { + response: http_response, + device_code: None, + }) + .unwrap(); + return Ok(receiver); } - return match next { - None => unreachable!(), - Some(auth_event) => match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(WebViewExecutionError::InvalidRedirectUri(reason)) - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let query = uri.query().or(uri.fragment()).ok_or( - WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), - )?; - - let response_query: AuthorizationQueryResponse = - serde_urlencoded::from_str(query)?; - Ok(response_query) - } - InteractiveAuthEvent::ClosingWindow(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => { - Err(WebViewExecutionError::WindowClosedRequested) - } - WindowCloseReason::InvalidWindowNavigation => { - Err(WebViewExecutionError::WindowClosedOnInvalidNavigation) + if let Some(json) = http_response.json() { + let device_code_response: DeviceCode = + serde_json::from_value(json).map_err(AuthExecutionError::from)?; + + if device_code_response.error.is_some() { + sender + .send(InteractiveDeviceCodeEvent::FailedAuth { + response: http_response, + device_code: Some(device_code_response), + }) + .unwrap(); + return Ok(receiver); + } else { + sender + .send(InteractiveDeviceCodeEvent::BeginAuth { + response: http_response, + device_code: Some(device_code_response.clone()), + }) + .unwrap(); + } + + let device_code = device_code_response.device_code; + let interval = Duration::from_secs(device_code_response.interval); + credential.with_device_code(device_code); + + let sender2 = sender.clone(); + let _ = std::thread::spawn(move || { + let mut should_slow_down = false; + + loop { + // Wait the amount of seconds that interval is. + if should_slow_down { + should_slow_down = false; + std::thread::sleep(interval.add(Duration::from_secs(5))); + } else { + std::thread::sleep(interval); + } + + let response = credential.execute().unwrap(); + let http_response = response.into_http_response()?; + let status = http_response.status(); + + if status.is_success() { + let json = http_response.json().unwrap(); + let token: Token = serde_json::from_value(json)?; + let cache_id = credential.app_config.cache_id.clone(); + credential.token_cache.store(cache_id, token); + sender2.send(InteractiveDeviceCodeEvent::SuccessfulAuthEvent { + response: http_response, + public_application: PublicClientApplication::from(credential), + })?; + break; + } else { + let json = http_response.json().unwrap(); + let option_error = json["error"].as_str().map(|value| value.to_owned()); + + if let Some(error) = option_error { + match PollDeviceCodeEvent::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeEvent::AuthorizationPending => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::AuthorizationPending, + }, + )?; + continue; + } + PollDeviceCodeEvent::AuthorizationDeclined => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::AuthorizationDeclined, + }, + )?; + break; + } + PollDeviceCodeEvent::BadVerificationCode => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::BadVerificationCode, + }, + )?; + continue; + } + PollDeviceCodeEvent::ExpiredToken => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::ExpiredToken, + }, + )?; + break; + } + PollDeviceCodeEvent::AccessDenied => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::AccessDenied, + }, + )?; + break; + } + PollDeviceCodeEvent::SlowDown => { + sender2.send( + InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::SlowDown, + }, + )?; + + should_slow_down = true; + continue; + } + }, + Err(_) => break, + } + } else { + // Body should have error or we should bail. + break; } - WindowCloseReason::TimedOut { - start: _, - requested_resume: _, - } => Err(WebViewExecutionError::WindowClosedOnTimeoutReached), } } - }, - }; + Ok::<(), anyhow::Error>(()) + }); + + // Spawn thread for webview + let sender3 = sender.clone(); + std::thread::spawn(move || { + InteractiveWebView::device_code_interactive_authentication( + Url::parse(device_code_response.verification_uri.as_str()).unwrap(), + options.unwrap_or_default(), + sender3, + ) + .unwrap(); + }); + } else { + sender + .send(InteractiveDeviceCodeEvent::FailedAuth { + response: http_response, + device_code: None, + }) + .unwrap(); + return Ok(receiver); + } + + Ok(receiver) } pub fn poll(&mut self) -> AuthExecutionResult<std::sync::mpsc::Receiver<JsonHttpResponse>> { @@ -464,14 +672,14 @@ impl DeviceCodePollingExecutor { sender.send(http_response)?; if let Some(error) = option_error { - match PollDeviceCodeType::from_str(error.as_str()) { + match PollDeviceCodeEvent::from_str(error.as_str()) { Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeType::AuthorizationPending => continue, - PollDeviceCodeType::AuthorizationDeclined => break, - PollDeviceCodeType::BadVerificationCode => continue, - PollDeviceCodeType::ExpiredToken => break, - PollDeviceCodeType::AccessDenied => break, - PollDeviceCodeType::SlowDown => { + PollDeviceCodeEvent::AuthorizationPending => continue, + PollDeviceCodeEvent::AuthorizationDeclined => break, + PollDeviceCodeEvent::BadVerificationCode => continue, + PollDeviceCodeEvent::ExpiredToken => break, + PollDeviceCodeEvent::AccessDenied => break, + PollDeviceCodeEvent::SlowDown => { should_slow_down = true; continue; } @@ -551,14 +759,14 @@ impl DeviceCodePollingExecutor { .await?; if let Some(error) = option_error { - match PollDeviceCodeType::from_str(error.as_str()) { + match PollDeviceCodeEvent::from_str(error.as_str()) { Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeType::AuthorizationPending => continue, - PollDeviceCodeType::AuthorizationDeclined => break, - PollDeviceCodeType::BadVerificationCode => continue, - PollDeviceCodeType::ExpiredToken => break, - PollDeviceCodeType::AccessDenied => break, - PollDeviceCodeType::SlowDown => { + PollDeviceCodeEvent::AuthorizationPending => continue, + PollDeviceCodeEvent::AuthorizationDeclined => break, + PollDeviceCodeEvent::BadVerificationCode => continue, + PollDeviceCodeEvent::ExpiredToken => break, + PollDeviceCodeEvent::AccessDenied => break, + PollDeviceCodeEvent::SlowDown => { should_slow_down = true; continue; } @@ -585,6 +793,7 @@ pub(crate) mod web_view_authenticator { InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, }; use graph_error::WebViewResult; + use url::Url; impl InteractiveAuthenticator for DeviceCodeCredential { fn interactive_authentication( @@ -596,14 +805,13 @@ pub(crate) mod web_view_authenticator { .azure_cloud_instance .auth_uri(&self.app_config.authority) .expect("Internal Error Please Report"); - let redirect_uri = self.app_config.redirect_uri.clone().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { InteractiveWebView::interactive_authentication( uri, - vec![redirect_uri], + vec![Url::parse("http://localhost:8080").unwrap()], web_view_options, sender, ) diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 9e568ae9..be86359c 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -339,11 +339,11 @@ impl OpenIdCredentialBuilder { .redirect_uri( Url::parse("http://localhost").expect("Internal Error - please report"), ) + .scope(vec!["openid"]) .build(), authorization_code: None, refresh_token: None, client_secret: String::new(), - scope: vec!["openid".to_owned()], code_verifier: None, pkce: None, serializer: Default::default(), @@ -352,14 +352,14 @@ impl OpenIdCredentialBuilder { } } - fn new_with_app_config(app_config: AppConfig) -> OpenIdCredentialBuilder { + fn new_with_app_config(mut app_config: AppConfig) -> OpenIdCredentialBuilder { + app_config.scope.insert("openid".to_string()); Self { credential: OpenIdCredential { app_config, authorization_code: None, refresh_token: None, client_secret: String::new(), - scope: vec!["openid".to_owned()], code_verifier: None, pkce: None, serializer: Default::default(), @@ -371,15 +371,15 @@ impl OpenIdCredentialBuilder { pub(crate) fn new_with_auth_code_and_secret( authorization_code: impl AsRef<str>, client_secret: impl AsRef<str>, - app_config: AppConfig, + mut app_config: AppConfig, ) -> OpenIdCredentialBuilder { + app_config.scope.insert("openid".to_string()); OpenIdCredentialBuilder { credential: OpenIdCredential { app_config, authorization_code: Some(authorization_code.as_ref().to_owned()), refresh_token: None, client_secret: client_secret.as_ref().to_owned(), - scope: vec![], code_verifier: None, pkce: None, serializer: Default::default(), diff --git a/graph-oauth/src/identity/device_code.rs b/graph-oauth/src/identity/device_code.rs index 6d7de299..2c6590bb 100644 --- a/graph-oauth/src/identity/device_code.rs +++ b/graph-oauth/src/identity/device_code.rs @@ -1,6 +1,10 @@ use std::collections::{BTreeSet, HashMap}; use std::str::FromStr; +use crate::identity::PublicClientApplication; +use crate::oauth::DeviceCodeCredential; +use crate::web::InteractiveAuthEvent; +use graph_core::http::JsonHttpResponse; use serde_json::Value; /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 @@ -53,7 +57,7 @@ fn default_interval() -> u64 { /// Response types used when polling for a device code /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum PollDeviceCodeType { +pub enum PollDeviceCodeEvent { /// The user hasn't finished authenticating, but hasn't canceled the flow. /// Repeat the request after at least interval seconds. AuthorizationPending, @@ -81,18 +85,39 @@ pub enum PollDeviceCodeType { SlowDown, } -impl FromStr for PollDeviceCodeType { +impl FromStr for PollDeviceCodeEvent { type Err = (); fn from_str(s: &str) -> Result<Self, Self::Err> { match s { - "authorization_pending" => Ok(PollDeviceCodeType::AuthorizationPending), - "authorization_declined" => Ok(PollDeviceCodeType::AuthorizationDeclined), - "bad_verification_code" => Ok(PollDeviceCodeType::BadVerificationCode), - "expired_token" => Ok(PollDeviceCodeType::ExpiredToken), - "access_denied" => Ok(PollDeviceCodeType::AccessDenied), - "slow_down" => Ok(PollDeviceCodeType::SlowDown), + "authorization_pending" => Ok(PollDeviceCodeEvent::AuthorizationPending), + "authorization_declined" => Ok(PollDeviceCodeEvent::AuthorizationDeclined), + "bad_verification_code" => Ok(PollDeviceCodeEvent::BadVerificationCode), + "expired_token" => Ok(PollDeviceCodeEvent::ExpiredToken), + "access_denied" => Ok(PollDeviceCodeEvent::AccessDenied), + "slow_down" => Ok(PollDeviceCodeEvent::SlowDown), _ => Err(()), } } } + +#[derive(Debug)] +pub enum InteractiveDeviceCodeEvent { + BeginAuth { + response: JsonHttpResponse, + device_code: Option<DeviceCode>, + }, + FailedAuth { + response: JsonHttpResponse, + device_code: Option<DeviceCode>, + }, + PollDeviceCode { + poll_device_code_event: PollDeviceCodeEvent, + response: JsonHttpResponse, + }, + InteractiveAuthEvent(InteractiveAuthEvent), + SuccessfulAuthEvent { + response: JsonHttpResponse, + public_application: PublicClientApplication<DeviceCodeCredential>, + }, +} diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index 788a4569..21186d5e 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -24,5 +24,5 @@ pub enum WindowCloseReason { pub enum InteractiveAuthEvent { InvalidRedirectUri(String), ReachedRedirectUri(Url), - ClosingWindow(WindowCloseReason), + WindowClosed(WindowCloseReason), } diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 76fb39d6..7661498b 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,6 +1,7 @@ use std::time::Duration; use url::Url; +use crate::oauth::InteractiveDeviceCodeEvent; use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; use graph_error::{WebViewExecutionError, WebViewResult}; use wry::application::event_loop::EventLoopBuilder; @@ -116,7 +117,7 @@ impl InteractiveWebView { let sender2 = sender.clone(); let window = WindowBuilder::new() - .with_title("Sign In") + .with_title(options.window_title) .with_closable(true) .with_content_protection(true) .with_minimizable(true) @@ -160,12 +161,16 @@ impl InteractiveWebView { .build()?; event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::WaitUntil(options.timeout); + if let Some(timeout) = options.timeout.as_ref() { + *control_flow = ControlFlow::WaitUntil(timeout.clone()); + } else { + *control_flow = ControlFlow::Wait; + } match event { Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { - sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::TimedOut { + sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { start, requested_resume })).unwrap_or_default(); tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); @@ -182,7 +187,119 @@ impl InteractiveWebView { event: WindowEvent::CloseRequested, .. } => { - sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::CloseRequested)).unwrap_or_default(); + sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); + tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { + tracing::error!(target: "interactive_webview", "WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); + if options.close_window_on_invalid_uri_navigation { + tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); + sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::InvalidWindowNavigation)).unwrap_or_default(); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_secs(1)); + + *control_flow = ControlFlow::Exit; + } + } + _ => (), + } + }); + } + + #[tracing::instrument] + pub fn device_code_interactive_authentication( + uri: Url, + options: WebViewOptions, + sender: std::sync::mpsc::Sender<InteractiveDeviceCodeEvent>, + ) -> anyhow::Result<()> { + tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); + //let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; + let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build(); + let proxy = event_loop.create_proxy(); + let sender2 = sender.clone(); + + let window = WindowBuilder::new() + .with_title(options.window_title) + .with_closable(true) + .with_content_protection(true) + .with_minimizable(true) + .with_maximizable(true) + .with_focused(true) + .with_resizable(true) + .with_theme(options.theme) + .build(&event_loop)?; + + let webview = WebViewBuilder::new(window)? + .with_url(uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + sender2 + .send(InteractiveDeviceCodeEvent::InteractiveAuthEvent( + InteractiveAuthEvent::ReachedRedirectUri(url.clone()), + )) + .unwrap_or_default(); + } + true + }) + .build()?; + + event_loop.run(move |event, _, control_flow| { + if let Some(timeout) = options.timeout.as_ref() { + *control_flow = ControlFlow::WaitUntil(timeout.clone()); + } else { + *control_flow = ControlFlow::Wait; + } + + match event { + Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), + Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { + sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { + start, requested_resume + }))).unwrap_or_default(); + tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested))).unwrap_or_default(); tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); if options.clear_browsing_data { @@ -209,7 +326,7 @@ impl InteractiveWebView { tracing::error!(target: "interactive_webview", "WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); if options.close_window_on_invalid_uri_navigation { tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); - sender.send(InteractiveAuthEvent::ClosingWindow(WindowCloseReason::InvalidWindowNavigation)).unwrap_or_default(); + sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::InvalidWindowNavigation))).unwrap_or_default(); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index 57404ab3..4ec6429b 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -24,7 +24,7 @@ pub struct WebViewOptions { /// when that timeout is reached. For instance, if your app is waiting on the /// user to log in and the user has not logged in after 20 minutes you may /// want to assume the user is idle in some way and close out of the webview window. - pub timeout: Instant, + pub timeout: Option<Instant>, /// The webview can store the cookies that were set after sign in so that on the next /// sign in the user is automatically logged in through SSO. Or you can clear the browsing /// data, cookies in this case, after sign in when the webview window closes. @@ -69,7 +69,7 @@ impl WebViewOptions { /// user to log in and the user has not logged in after 20 minutes you may /// want to assume the user is idle in some way and close out of the webview window. pub fn with_timeout(mut self, instant: Instant) -> Self { - self.timeout = instant; + self.timeout = Some(instant); self } @@ -90,7 +90,7 @@ impl Default for WebViewOptions { theme: None, ports: vec![], // 10 Minutes default timeout - timeout: Instant::now().add(Duration::from_secs(10 * 60)), + timeout: None, clear_browsing_data: false, } } From b911495b800712b26ba6f62fd1651136dba67867 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:18:41 -0500 Subject: [PATCH 058/118] Add interactive authentication to device code credential --- Cargo.toml | 9 +- examples/oauth/device_code.rs | 3 +- graph-core/Cargo.toml | 1 + graph-core/src/cache/in_memory_cache_store.rs | 9 +- graph-error/src/webview_error.rs | 12 +- graph-oauth/Cargo.toml | 11 +- .../src/identity/authorization_response.rs | 12 + .../credentials/application_builder.rs | 10 +- .../auth_code_authorization_url.rs | 20 +- .../authorization_code_credential.rs | 28 - .../confidential_client_application.rs | 12 +- .../credentials/device_code_credential.rs | 595 +++++++++--------- .../legacy/code_flow_authorization_url.rs | 122 ---- .../legacy/code_flow_credential.rs | 208 ------ .../src/identity/credentials/legacy/mod.rs | 6 - .../legacy/token_flow_authorization_url.rs | 109 ---- .../credentials/public_client_application.rs | 3 +- ...de.rs => device_authorization_response.rs} | 86 ++- graph-oauth/src/identity/mod.rs | 30 +- graph-oauth/src/identity/token.rs | 22 + graph-oauth/src/lib.rs | 4 +- graph-oauth/src/web/interactive_web_view.rs | 50 +- graph-oauth/src/web/web_view_options.rs | 3 +- src/client/graph.rs | 8 +- 24 files changed, 463 insertions(+), 910 deletions(-) create mode 100644 graph-oauth/src/identity/authorization_response.rs delete mode 100644 graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs delete mode 100644 graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs delete mode 100644 graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs rename graph-oauth/src/identity/{device_code.rs => device_authorization_response.rs} (59%) diff --git a/Cargo.toml b/Cargo.toml index 7f96d054..149eb976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" lazy_static = "1.4.0" -uuid = { version = "1.4.1", features = ["v4"] } graph-oauth = { path = "./graph-oauth", version = "1.0.2", default-features=false } graph-http = { path = "./graph-http", version = "1.1.0", default-features=false } @@ -56,7 +55,7 @@ brotli = ["reqwest/brotli", "graph-http/brotli", "graph-oauth/brotli", "graph-co deflate = ["reqwest/deflate", "graph-http/deflate", "graph-oauth/deflate", "graph-core/deflate"] trust-dns = ["reqwest/trust-dns", "graph-http/trust-dns", "graph-oauth/trust-dns", "graph-core/trust-dns"] openssl = ["graph-oauth/openssl"] -# interactive-auth = ["graph-oauth/interactive-auth"] +interactive-auth = ["graph-oauth/interactive-auth"] [dev-dependencies] bytes = { version = "1.4.0" } @@ -70,7 +69,6 @@ anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.4" from_as = "0.2.0" -tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } graph-codegen = { path = "./graph-codegen", version = "0.0.1" } @@ -83,3 +81,8 @@ debug = false name = "oauth_certificate_main" path = "examples/oauth_certificate/main.rs" required-features = ["openssl"] + +[[example]] +name = "interactive_auth_main" +path = "examples/interactive_authentication/main.rs" +required-features = ["interactive-auth"] diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index daebf929..b2a2e6e8 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -21,8 +21,9 @@ static TENANT: &str = "<TENANT>"; // device code endpoint is polled an access token is returned. fn poll_device_code() { let mut device_executor = PublicClientApplication::builder(CLIENT_ID) - .with_device_code_polling_executor() + .with_device_code_executor() .with_scope(vec!["User.Read"]) + .with_tenant(TENANT) .poll() .unwrap(); diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index ca384726..2bbbe311 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -14,6 +14,7 @@ base64 = "0.21.0" dyn-clone = "1.0.14" Inflector = "0.11.4" http = "0.2.9" +parking_lot = "0.12.1" percent-encoding = "2" reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" diff --git a/graph-core/src/cache/in_memory_cache_store.rs b/graph-core/src/cache/in_memory_cache_store.rs index 5762b96d..127d7ad2 100644 --- a/graph-core/src/cache/in_memory_cache_store.rs +++ b/graph-core/src/cache/in_memory_cache_store.rs @@ -1,6 +1,7 @@ use crate::cache::cache_store::CacheStore; +use parking_lot::RwLock; use std::collections::HashMap; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; #[derive(Clone, Default)] pub struct InMemoryCacheStore<Value: Clone> { @@ -17,20 +18,20 @@ impl<Value: Clone> InMemoryCacheStore<Value> { impl<Value: Clone> CacheStore<Value> for InMemoryCacheStore<Value> { fn store<T: Into<String>>(&mut self, cache_id: T, token: Value) { - let mut write_lock = self.store.write().unwrap(); + let mut write_lock = self.store.write(); write_lock.insert(cache_id.into(), token); drop(write_lock); } fn get(&self, cache_id: &str) -> Option<Value> { - let read_lock = self.store.read().unwrap(); + let read_lock = self.store.read(); let token = read_lock.get(cache_id).cloned(); drop(read_lock); token } fn evict(&self, cache_id: &str) -> Option<Value> { - let mut write_lock = self.store.write().unwrap(); + let mut write_lock = self.store.write(); let token = write_lock.remove(cache_id); drop(write_lock); token diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index d7187a70..9faf2b41 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -1,5 +1,5 @@ -use crate::{AuthExecutionError, AuthorizationFailure}; -use url::ParseError; +use crate::{AuthExecutionError, AuthorizationFailure, ErrorMessage}; +use std::sync::mpsc::RecvError; #[derive(Debug, thiserror::Error)] pub enum WebViewExecutionError { @@ -31,11 +31,17 @@ pub enum WebViewExecutionError { /// the query or fragment of the url to a AuthorizationQueryResponse #[error("{0:#?}")] SerdeError(#[from] serde::de::value::Error), + + #[error("{0:#?}")] + RecvError(#[from] RecvError), /// Error from building out the parameters necessary for authorization /// this most likely came from an invalid parameter or missing parameter /// passed to the client used for building the url. #[error("{0:#?}")] AuthorizationError(#[from] AuthorizationFailure), + /// Error that happens calling the http request. + #[error("{0:#?}")] + AuthExecutionError(#[from] AuthExecutionError), } #[derive(Debug, thiserror::Error)] @@ -76,4 +82,6 @@ pub enum WebViewDeviceCodeExecutionError { /// Error that happens calling the http request. #[error("{0:#?}")] AuthExecutionError(#[from] AuthExecutionError), + #[error("{0:#?}")] + DeviceCodeAuthFailed(http::Response<Result<serde_json::Value, ErrorMessage>>), } diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 3622079b..05cad53b 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -19,8 +19,7 @@ exclude = [ anyhow = { version = "1.0.69", features = ["backtrace"]} async-trait = "0.1.35" base64 = "0.21.0" -chrono = { version = "0.4.23", features = ["serde"] } -chrono-humanize = "0.2.2" +either = "1.9.0" dyn-clone = "1.0.14" hex = "0.4.3" http = "0.2.9" @@ -30,15 +29,13 @@ serde = { version = "1", features = ["derive"] } serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" -strum = { version = "0.24.1", features = ["derive"] } +strum = { version = "0.25.0", features = ["derive"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } webbrowser = "0.8.7" -wry = { version = "0.33.1"} +wry = { version = "0.33.1", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } -hyper = { version = "1.0.0-rc.3", features = ["full"] } -http-body-util = "0.1.0-rc.2" tracing = "0.1.37" graph-error = { path = "../graph-error" } @@ -52,7 +49,7 @@ brotli = ["reqwest/brotli", "graph-core/brotli"] deflate = ["reqwest/deflate", "graph-core/deflate"] trust-dns = ["reqwest/trust-dns", "graph-core/trust-dns"] openssl = ["dep:openssl"] -# interactive-auth = ["dep:wry"] +interactive-auth = ["dep:wry"] [[test]] name = "x509_certificate_tests" diff --git a/graph-oauth/src/identity/authorization_response.rs b/graph-oauth/src/identity/authorization_response.rs new file mode 100644 index 00000000..0ac0c2ec --- /dev/null +++ b/graph-oauth/src/identity/authorization_response.rs @@ -0,0 +1,12 @@ +/// Representation of the authorization response described in the [specification](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2) +/// +/// +/// The [specification](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2) states: +/// If the resource owner grants the access request, the authorization +/// server issues an authorization code and delivers it to the client by +/// adding the following parameters to the query component of the +/// redirection URI using the "application/x-www-form-urlencoded" +pub struct AuthorizationResponse { + pub code: String, + pub state: String, +} diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 8d84fc86..7e9282e0 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -8,12 +8,10 @@ use crate::identity::{ PublicClientApplication, ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, }; -use base64::Engine; use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::env::VarError; -use uuid::Uuid; #[cfg(feature = "openssl")] use crate::identity::{ @@ -288,7 +286,7 @@ impl PublicClientApplicationBuilder { self } - pub fn with_device_code_polling_executor(self) -> DeviceCodePollingExecutor { + pub fn with_device_code_executor(self) -> DeviceCodePollingExecutor { DeviceCodePollingExecutor::new_with_app_config(self.app_config) } @@ -296,12 +294,6 @@ impl PublicClientApplicationBuilder { DeviceCodeCredentialBuilder::new_with_device_code(device_code.as_ref(), self.app_config) } - /* - pub fn interactive_authentication(self) -> DeviceCodeCredentialBuilder { - - } - */ - pub fn with_username_password( self, username: impl AsRef<str>, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 31a21d94..6e8e6ea6 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -8,20 +8,21 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; -use graph_error::{IdentityResult, WebViewExecutionError, WebViewResult, AF}; +use graph_error::{IdentityResult, AF}; use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, + credentials::app_config::AppConfig, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; -//#[cfg(feature = "interactive-auth")] -use crate::identity::AuthorizationQueryResponse; -use crate::oauth::Token; +#[cfg(feature = "interactive-auth")] +use graph_error::WebViewResult; -//#[cfg(feature = "interactive-auth")] +#[cfg(feature = "interactive-auth")] +use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResponse, Token}; + +#[cfg(feature = "interactive-auth")] use crate::web::{ InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, }; @@ -180,7 +181,7 @@ impl AuthCodeAuthorizationUrlParameters { self.nonce.as_ref() } - //#[cfg(feature = "interactive-auth")] + #[cfg(feature = "interactive-auth")] pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, @@ -227,7 +228,7 @@ impl AuthCodeAuthorizationUrlParameters { } } -// #[cfg(feature = "interactive-auth")] +#[cfg(feature = "interactive-auth")] pub(crate) mod web_view_authenticator { use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; use crate::web::{ @@ -531,6 +532,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } + #[cfg(feature = "interactive-auth")] pub fn with_interactive_authentication( &self, options: Option<WebViewOptions>, diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index ac6aae65..968a568a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -68,25 +68,6 @@ impl Debug for AuthorizationCodeCredential { } impl AuthorizationCodeCredential { - pub(crate) fn from_token_and_app_config( - app_config: AppConfig, - token: Token, - ) -> AuthorizationCodeCredential { - let cache_id = app_config.cache_id.clone(); - let mut token_cache = InMemoryCacheStore::new(); - token_cache.store(cache_id, token); - - AuthorizationCodeCredential { - app_config, - authorization_code: None, - refresh_token: None, - client_secret: "".to_string(), - code_verifier: None, - serializer: Default::default(), - token_cache, - } - } - fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; let new_token: Token = response.json()?; @@ -301,15 +282,6 @@ impl AuthorizationCodeCredentialBuilder { } } - pub(crate) fn new_with_token( - app_config: AppConfig, - token: Token, - ) -> AuthorizationCodeCredentialBuilder { - Self { - credential: AuthorizationCodeCredential::from_token_and_app_config(app_config, token), - } - } - pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self.credential.refresh_token = None; diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 700a0be2..7c8081ab 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -9,32 +9,28 @@ use uuid::Uuid; use graph_core::cache::{AsBearer, TokenCache}; use graph_core::identity::ClientApplication; -use graph_error::{AuthExecutionResult, IdentityResult, AF}; +use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::{ credentials::app_config::AppConfig, credentials::application_builder::ConfidentialClientApplicationBuilder, credentials::client_assertion_credential::ClientAssertionCredential, Authority, AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, - AuthorizationCodeCredential, AuthorizationQueryResponse, AzureCloudInstance, - ClientCertificateCredential, ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, -}; -use crate::oauth::AuthCodeAuthorizationUrlParameters; -use crate::web::{ - InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, + AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, + ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, }; /// Clients capable of maintaining the confidentiality of their credentials /// (e.g., client implemented on a secure server with restricted access to the client credentials), /// or capable of secure client authentication using other means. /// +/// See [Client Types](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1) in the specification. /// /// # Build a confidential client for the authorization code grant. /// Use [with_authorization_code](crate::identity::ConfidentialClientApplicationBuilder::with_auth_code) to set the authorization code received from /// the authorization step, see [Request an authorization code](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) /// You can use the [AuthCodeAuthorizationUrlParameterBuilder](crate::identity::AuthCodeAuthorizationUrlParameterBuilder) /// to build the url that the user will be directed to authorize at. -/// /// ```rust #[derive(Clone, Debug)] pub struct ConfidentialClientApplication<Credential> { diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 026f7e65..4b728216 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -2,14 +2,12 @@ use async_trait::async_trait; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use std::ops::Add; -use std::process::exit; use std::str::FromStr; -use std::sync::mpsc::Receiver; -use std::thread; use std::time::Duration; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; +use tracing::error; use url::Url; use uuid::Uuid; @@ -18,18 +16,30 @@ use graph_core::http::{ }; use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthTaskExecutionResult, AuthorizationFailure, - DeviceCodeWebViewResult, IdentityResult, WebViewExecutionError, WebViewResult, AF, + IdentityResult, }; use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationQueryResponse, AzureCloudInstance, DeviceCode, ForceTokenRefresh, - PollDeviceCodeEvent, PublicClientApplication, TokenCredentialExecutor, + Authority, AzureCloudInstance, DeviceAuthorizationResponse, ForceTokenRefresh, + PollDeviceCodeEvent, PublicClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth::{InteractiveDeviceCodeEvent, Token}; -use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; -use crate::web::{InteractiveAuthenticator, InteractiveWebView}; + +#[cfg(feature = "interactive-auth")] +use crate::oauth::InteractiveDeviceCodeEvent; + +#[cfg(feature = "interactive-auth")] +use graph_error::WebViewResult; + +#[cfg(feature = "interactive-auth")] +use crate::web::{IInteractiveWebView, InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; + +#[cfg(feature = "interactive-auth")] +use std::sync::mpsc::{Receiver, Sender}; + +#[cfg(feature = "interactive-auth")] +use std::thread; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -38,11 +48,13 @@ credential_builder!( PublicClientApplication<DeviceCodeCredential> ); -/// Allows users to sign in to input-constrained devices such as a smart TV, IoT device, -/// or a printer. To enable this flow, the device has the user visit a webpage in a browser on -/// another device to sign in. Once the user signs in, the device is able to get access tokens -/// and refresh tokens as needed. -/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code +/// The device authorization grant: allows users to sign in to input-constrained devices +/// such as a smart TV, IoT device, or a printer. To enable this flow, the device has the +/// user visit a webpage in a browser on another device to sign in. Once the user signs in, +/// the device is able to get access tokens and refresh tokens as needed. +/// +/// For more info on the protocol supported by the Microsoft Identity Platform see the +/// [Microsoft identity platform and the OAuth 2.0 device authorization grant flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) #[derive(Clone)] pub struct DeviceCodeCredential { pub(crate) app_config: AppConfig, @@ -208,10 +220,6 @@ impl TokenCache for DeviceCodeCredential { impl TokenCredentialExecutor for DeviceCodeCredential { fn uri(&mut self) -> IdentityResult<Url> { - let azure_cloud_instance = self.azure_cloud_instance(); - self.serializer - .authority_device_code(&azure_cloud_instance, &self.authority()); - if self.device_code.is_none() && self.refresh_token.is_none() { Ok(self .azure_cloud_instance() @@ -344,23 +352,16 @@ impl DeviceCodeCredentialBuilder { } } -/* -impl From<&DeviceCode> for DeviceCodeCredentialBuilder { - fn from(value: &DeviceCode) -> Self { - DeviceCodeCredentialBuilder { - credential: DeviceCodeCredential { - app_config: AppConfig::new(), - refresh_token: None, - device_code: Some(value.device_code.clone()), - scope: vec![], - serializer: Default::default(), - token_cache: Default::default(), - }, - } - } +#[cfg(feature = "interactive-auth")] +#[derive(Debug)] +pub enum DeviceCodeInteractiveEvent { + DeviceAuthorization(DeviceAuthorizationResponse), + Failed(JsonHttpResponse), + WindowClosed(WindowCloseReason), + Success(PublicClientApplication<DeviceCodeCredential>), } - */ +#[derive(Debug)] pub struct DeviceCodePollingExecutor { credential: DeviceCodeCredential, } @@ -378,259 +379,16 @@ impl DeviceCodePollingExecutor { } } - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(mut self, scope: I) -> Self { self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } - pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { + pub fn with_tenant(mut self, tenant_id: impl AsRef<str>) -> Self { self.credential.app_config.tenant_id = Some(tenant_id.as_ref().to_owned()); self } - pub fn interactive_webview_auth(&mut self, options: Option<WebViewOptions>) { - let executor_result = self.interactive_webview_authentication(options); - if let Ok(mut executor) = executor_result { - while let interactive_device_code_event = executor.recv().unwrap() { - match interactive_device_code_event { - InteractiveDeviceCodeEvent::BeginAuth { - response, - device_code, - } => { - println!("{:#?}", response); - println!("{:#?}", device_code); - } - InteractiveDeviceCodeEvent::FailedAuth { - response, - device_code, - } => { - println!("{:#?}", response); - println!("{:#?}", device_code); - } - InteractiveDeviceCodeEvent::PollDeviceCode { - poll_device_code_event, - response, - } => { - println!("{:#?}", response); - println!("{:#?}", poll_device_code_event); - } - InteractiveDeviceCodeEvent::InteractiveAuthEvent(auth_event) => { - match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(_) => {} - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - println!("{:#?}", uri); - } - InteractiveAuthEvent::WindowClosed(window_closed) => { - match window_closed { - WindowCloseReason::CloseRequested => { - println!("close requested"); - } - WindowCloseReason::InvalidWindowNavigation => { - println!("invalid navigation"); - } - WindowCloseReason::TimedOut { - requested_resume, - start, - } => { - println!("Timed Out"); - } - } - } - } - } - InteractiveDeviceCodeEvent::SuccessfulAuthEvent { - response, - public_application, - } => { - println!("{:#?}", response); - let json = response.json().unwrap(); - let token: Token = serde_json::from_value(json).unwrap(); - println!("{:#?}", token); - } - } - } - } else if let Err(err) = executor_result { - println!("{err:#?}"); - } - } - - pub fn interactive_webview_authentication( - &mut self, - options: Option<WebViewOptions>, - ) -> DeviceCodeWebViewResult<Receiver<InteractiveDeviceCodeEvent>> { - let (sender, receiver) = std::sync::mpsc::channel(); - - let mut credential = self.credential.clone(); - let response = credential.execute()?; - - let http_response = response.into_http_response()?; - - if !http_response.status().is_success() { - sender - .send(InteractiveDeviceCodeEvent::FailedAuth { - response: http_response, - device_code: None, - }) - .unwrap(); - return Ok(receiver); - } - - if let Some(json) = http_response.json() { - let device_code_response: DeviceCode = - serde_json::from_value(json).map_err(AuthExecutionError::from)?; - - if device_code_response.error.is_some() { - sender - .send(InteractiveDeviceCodeEvent::FailedAuth { - response: http_response, - device_code: Some(device_code_response), - }) - .unwrap(); - return Ok(receiver); - } else { - sender - .send(InteractiveDeviceCodeEvent::BeginAuth { - response: http_response, - device_code: Some(device_code_response.clone()), - }) - .unwrap(); - } - - let device_code = device_code_response.device_code; - let interval = Duration::from_secs(device_code_response.interval); - credential.with_device_code(device_code); - - let sender2 = sender.clone(); - let _ = std::thread::spawn(move || { - let mut should_slow_down = false; - - loop { - // Wait the amount of seconds that interval is. - if should_slow_down { - should_slow_down = false; - std::thread::sleep(interval.add(Duration::from_secs(5))); - } else { - std::thread::sleep(interval); - } - - let response = credential.execute().unwrap(); - let http_response = response.into_http_response()?; - let status = http_response.status(); - - if status.is_success() { - let json = http_response.json().unwrap(); - let token: Token = serde_json::from_value(json)?; - let cache_id = credential.app_config.cache_id.clone(); - credential.token_cache.store(cache_id, token); - sender2.send(InteractiveDeviceCodeEvent::SuccessfulAuthEvent { - response: http_response, - public_application: PublicClientApplication::from(credential), - })?; - break; - } else { - let json = http_response.json().unwrap(); - let option_error = json["error"].as_str().map(|value| value.to_owned()); - - if let Some(error) = option_error { - match PollDeviceCodeEvent::from_str(error.as_str()) { - Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeEvent::AuthorizationPending => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::AuthorizationPending, - }, - )?; - continue; - } - PollDeviceCodeEvent::AuthorizationDeclined => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::AuthorizationDeclined, - }, - )?; - break; - } - PollDeviceCodeEvent::BadVerificationCode => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::BadVerificationCode, - }, - )?; - continue; - } - PollDeviceCodeEvent::ExpiredToken => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::ExpiredToken, - }, - )?; - break; - } - PollDeviceCodeEvent::AccessDenied => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::AccessDenied, - }, - )?; - break; - } - PollDeviceCodeEvent::SlowDown => { - sender2.send( - InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::SlowDown, - }, - )?; - - should_slow_down = true; - continue; - } - }, - Err(_) => break, - } - } else { - // Body should have error or we should bail. - break; - } - } - } - Ok::<(), anyhow::Error>(()) - }); - - // Spawn thread for webview - let sender3 = sender.clone(); - std::thread::spawn(move || { - InteractiveWebView::device_code_interactive_authentication( - Url::parse(device_code_response.verification_uri.as_str()).unwrap(), - options.unwrap_or_default(), - sender3, - ) - .unwrap(); - }); - } else { - sender - .send(InteractiveDeviceCodeEvent::FailedAuth { - response: http_response, - device_code: None, - }) - .unwrap(); - return Ok(receiver); - } - - Ok(receiver) - } - pub fn poll(&mut self) -> AuthExecutionResult<std::sync::mpsc::Receiver<JsonHttpResponse>> { let (sender, receiver) = std::sync::mpsc::channel(); @@ -639,7 +397,7 @@ impl DeviceCodePollingExecutor { let http_response = response.into_http_response()?; let json = http_response.json().unwrap(); - let device_code_response: DeviceCode = serde_json::from_value(json)?; + let device_code_response: DeviceAuthorizationResponse = serde_json::from_value(json)?; sender.send(http_response).unwrap(); @@ -684,7 +442,13 @@ impl DeviceCodePollingExecutor { continue; } }, - Err(_) => break, + Err(_) => { + error!( + target = "device_code_polling_executor", + "Invalid PollDeviceCodeEvent" + ); + break; + } } } else { // Body should have error or we should bail. @@ -716,7 +480,7 @@ impl DeviceCodePollingExecutor { let http_response = response.into_http_response_async().await?; let json = http_response.json().unwrap(); - let device_code_response: DeviceCode = + let device_code_response: DeviceAuthorizationResponse = serde_json::from_value(json).map_err(AuthExecutionError::from)?; sender @@ -784,43 +548,246 @@ impl DeviceCodePollingExecutor { Ok(receiver) } + + #[cfg(feature = "interactive-auth")] + pub fn execute_interactive_authentication( + &mut self, + ) -> AuthExecutionResult<DeviceCodeInteractiveAuth> { + let response = self.credential.execute()?; + let device_authorization_response: DeviceAuthorizationResponse = response.json()?; + Ok(DeviceCodeInteractiveAuth { + credential: self.credential.clone(), + device_authorization_response, + }) + } } -// #[cfg(feature = "interactive-auth")] -pub(crate) mod web_view_authenticator { - use crate::oauth::DeviceCodeCredential; - use crate::web::{ - InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, - }; - use graph_error::WebViewResult; - use url::Url; - - impl InteractiveAuthenticator for DeviceCodeCredential { - fn interactive_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { - let uri = self - .app_config - .azure_cloud_instance - .auth_uri(&self.app_config.authority) - .expect("Internal Error Please Report"); - let web_view_options = interactive_web_view_options.unwrap_or_default(); - let (sender, receiver) = std::sync::mpsc::channel(); - - std::thread::spawn(move || { - InteractiveWebView::interactive_authentication( - uri, - vec![Url::parse("http://localhost:8080").unwrap()], - web_view_options, - sender, - ) - .unwrap(); - }); - - Ok(receiver) +#[cfg(feature = "interactive-auth")] +#[derive(Debug)] +pub struct DeviceCodeInteractiveAuth { + credential: DeviceCodeCredential, + pub device_authorization_response: DeviceAuthorizationResponse, +} + +#[cfg(feature = "interactive-auth")] +impl DeviceCodeInteractiveAuth { + pub(crate) fn new( + credential: DeviceCodeCredential, + device_authorization_response: DeviceAuthorizationResponse, + ) -> DeviceCodeInteractiveAuth { + DeviceCodeInteractiveAuth { + credential, + device_authorization_response, + } + } + + pub fn begin( + &mut self, + options: Option<WebViewOptions>, + ) -> WebViewResult<Receiver<DeviceCodeInteractiveEvent>> { + let executor = self.interactive_webview_authentication(options)?; + let (sender, receiver) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + DeviceCodePollingExecutor::execute_interactive_loop(sender, executor); + }); + + Ok(receiver) + } + + #[tracing::instrument] + fn execute_interactive_loop( + sender: Sender<DeviceCodeInteractiveEvent>, + executor: Receiver<InteractiveDeviceCodeEvent>, + ) { + loop { + match executor.recv() { + Ok(interactive_device_code_event) => match interactive_device_code_event { + InteractiveDeviceCodeEvent::PollDeviceCode { + poll_device_code_event, + response, + } => { + let res = response.json().unwrap_or_default().to_string(); + tracing::debug!(target: "device_code_polling_executor", poll_device_code = poll_device_code_event.as_str(), http_response = res); + + match poll_device_code_event { + PollDeviceCodeEvent::AuthorizationPending + | PollDeviceCodeEvent::SlowDown => continue, + PollDeviceCodeEvent::AuthorizationDeclined + | PollDeviceCodeEvent::BadVerificationCode + | PollDeviceCodeEvent::ExpiredToken + | PollDeviceCodeEvent::AccessDenied => { + sender + .send(DeviceCodeInteractiveEvent::Failed(response)) + .unwrap_or_default(); + break; + } + } + } + InteractiveDeviceCodeEvent::WindowClosed(window_closed) => { + sender + .send(DeviceCodeInteractiveEvent::WindowClosed(window_closed)) + .unwrap_or_default(); + break; + } + InteractiveDeviceCodeEvent::SuccessfulAuthEvent { + response, + public_application, + } => { + tracing::debug!(target: "device_code_polling_executor", "PublicApplication: {public_application:#?}"); + sender + .send(DeviceCodeInteractiveEvent::Success(public_application)) + .unwrap_or_default(); + } + _ => {} + }, + Err(err) => panic!("{}", err), + } } } + + #[tracing::instrument] + pub fn interactive_webview_authentication( + &mut self, + options: Option<WebViewOptions>, + ) -> WebViewResult<Receiver<InteractiveDeviceCodeEvent>> { + let (sender, receiver) = std::sync::mpsc::channel(); + let mut credential = self.credential.clone(); + let device_authorization_response = self.device_authorization_response.clone(); + + // Spawn thread for webview + let sender3 = sender.clone(); + std::thread::spawn(move || { + let url = { + if let Some(url_complete) = device_authorization_response + .verification_uri_complete + .as_ref() + { + Url::parse(url_complete).unwrap() + } else { + Url::parse(device_authorization_response.verification_uri.as_str()).unwrap() + } + }; + + InteractiveWebView::device_code_interactive_authentication( + url, + options.unwrap_or_default(), + sender3, + ) + .unwrap(); + }); + + let device_code = device_authorization_response.device_code; + let interval = Duration::from_secs(device_authorization_response.interval); + credential.with_device_code(device_code); + + let sender2 = sender.clone(); + std::thread::spawn(move || { + let mut should_slow_down = false; + + loop { + // Wait the amount of seconds that interval is. + if should_slow_down { + should_slow_down = false; + std::thread::sleep(interval.add(Duration::from_secs(5))); + } else { + std::thread::sleep(interval); + } + + let response = credential.execute().unwrap(); + tracing::debug!(target: "device_code_polling_executor", "{response:#?}"); + let http_response = response.into_http_response()?; + let status = http_response.status(); + + if status.is_success() { + let json = http_response.json().unwrap(); + let token: Token = serde_json::from_value(json)?; + let cache_id = credential.app_config.cache_id.clone(); + credential.token_cache.store(cache_id, token); + sender2.send(InteractiveDeviceCodeEvent::SuccessfulAuthEvent { + response: http_response, + public_application: PublicClientApplication::from(credential), + })?; + break; + } else { + let json = http_response.json().unwrap(); + let option_error = json["error"].as_str().map(|value| value.to_owned()); + + if let Some(error) = option_error { + match PollDeviceCodeEvent::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeEvent::AuthorizationPending => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::AuthorizationPending, + })?; + continue; + } + PollDeviceCodeEvent::AuthorizationDeclined => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::AuthorizationDeclined, + })?; + break; + } + PollDeviceCodeEvent::BadVerificationCode => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: + PollDeviceCodeEvent::BadVerificationCode, + })?; + continue; + } + PollDeviceCodeEvent::ExpiredToken => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: PollDeviceCodeEvent::ExpiredToken, + })?; + break; + } + PollDeviceCodeEvent::AccessDenied => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: PollDeviceCodeEvent::AccessDenied, + })?; + break; + } + PollDeviceCodeEvent::SlowDown => { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: PollDeviceCodeEvent::SlowDown, + })?; + + should_slow_down = true; + continue; + } + }, + Err(err) => { + tracing::trace!(target: "device_code_polling_executor", "Error occurred while polling device code: {err:#?}"); + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: PollDeviceCodeEvent::AccessDenied, + })?; + break; + } + } + } else { + sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { + response: http_response, + poll_device_code_event: PollDeviceCodeEvent::AccessDenied, + })?; + // Body should have error or we should bail. + break; + } + } + } + Ok::<(), anyhow::Error>(()) + }); + + Ok(receiver) + } } #[cfg(test)] diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs deleted file mode 100644 index afedb2b7..00000000 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_authorization_url.rs +++ /dev/null @@ -1,122 +0,0 @@ -use url::Url; - -use graph_error::{AuthorizationFailure, IdentityResult}; - -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::ResponseType; - -/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive -/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. -/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow -#[derive(Clone)] -pub struct CodeFlowAuthorizationUrl { - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, - /// Required - /// The same redirect_uri value that was used to acquire the authorization_code. - pub(crate) redirect_uri: String, - /// Required - /// Must be code for code flow. - pub(crate) response_type: ResponseType, - /// Required - /// A space-separated list of scopes. The scopes must all be from a single resource, - /// along with OIDC scopes (profile, openid, email). For more information, see Permissions - /// and consent in the Microsoft identity platform. This parameter is a Microsoft extension - /// to the authorization code flow, intended to allow apps to declare the resource they want - /// the token for during token redemption. - pub(crate) scope: Vec<String>, -} - -impl CodeFlowAuthorizationUrl { - pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( - client_id: T, - redirect_uri: T, - scope: I, - ) -> CodeFlowAuthorizationUrl { - CodeFlowAuthorizationUrl { - client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), - response_type: ResponseType::Code, - scope: scope.into_iter().map(|s| s.to_string()).collect(), - } - } - - pub fn builder() -> CodeFlowAuthorizationUrlBuilder { - CodeFlowAuthorizationUrlBuilder::new() - } - - pub fn url(&self) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); - if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::result("redirect_uri"); - } - - if self.client_id.trim().is_empty() { - return AuthorizationFailure::result("client_id"); - } - - if self.scope.is_empty() { - return AuthorizationFailure::result("scope"); - } - - serializer - .client_id(self.client_id.as_str()) - .redirect_uri(self.redirect_uri.as_str()) - .extend_scopes(self.scope.clone()) - .legacy_authority() - .response_type(self.response_type.clone()); - - let query = serializer.encode_query( - vec![], - vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::Scope, - OAuthParameter::ResponseType, - ], - )?; - - if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(query.as_str())); - Ok(url) - } else { - AuthorizationFailure::msg_result("authorization_url", "Internal Error") - } - } -} - -#[derive(Clone)] -pub struct CodeFlowAuthorizationUrlBuilder { - authorization_url: CodeFlowAuthorizationUrl, -} - -impl CodeFlowAuthorizationUrlBuilder { - fn new() -> CodeFlowAuthorizationUrlBuilder { - CodeFlowAuthorizationUrlBuilder { - authorization_url: CodeFlowAuthorizationUrl { - client_id: String::new(), - redirect_uri: String::new(), - response_type: ResponseType::Code, - scope: vec![], - }, - } - } - - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.authorization_url.client_id = client_id.as_ref().to_owned(); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); - self - } -} diff --git a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs b/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs deleted file mode 100644 index 322bcfca..00000000 --- a/graph-oauth/src/identity/credentials/legacy/code_flow_credential.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::collections::HashMap; - -use url::Url; - -use graph_error::{AuthorizationFailure, IdentityResult}; - -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::identity::{Authority, AuthorizationSerializer, AzureCloudInstance}; - -/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive -/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. -/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#code-flow -#[derive(Clone, Eq, PartialEq)] -pub struct CodeFlowCredential { - /// Required unless requesting a refresh token - /// The authorization code obtained from a call to authorize. - /// The code should be obtained with all required scopes. - pub(crate) authorization_code: Option<String>, - /// Required when requesting a new access token using a refresh token - /// The refresh token needed to make an access token request using a refresh token. - /// Do not include an authorization code when using a refresh token. - pub(crate) refresh_token: Option<String>, - /// Required. - /// The Application (client) ID that the Azure portal - App registrations page assigned - /// to your app - pub(crate) client_id: String, - /// Required - /// The application secret that you created in the app registration portal for your app. - /// Don't use the application secret in a native app or single page app because a - /// client_secret can't be reliably stored on devices or web pages. It's required for web - /// apps and web APIs, which can store the client_secret securely on the server side. Like - /// all parameters here, the client secret must be URL-encoded before being sent. This step - /// is done by the SDK. For more information on URI encoding, see the URI Generic Syntax - /// specification. The Basic auth pattern of instead providing credentials in the Authorization - /// header, per RFC 6749 is also supported. - pub(crate) client_secret: String, - /// The same redirect_uri value that was used to acquire the authorization_code. - pub(crate) redirect_uri: String, - serializer: OAuthSerializer, -} - -impl CodeFlowCredential { - pub fn new<T: AsRef<str>>( - client_id: T, - client_secret: T, - authorization_code: T, - redirect_uri: T, - ) -> CodeFlowCredential { - CodeFlowCredential { - authorization_code: Some(authorization_code.as_ref().to_owned()), - refresh_token: None, - client_id: client_id.as_ref().to_owned(), - client_secret: client_secret.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), - serializer: OAuthSerializer::new(), - } - } - - pub fn builder() -> CodeFlowCredentialBuilder { - CodeFlowCredentialBuilder::new() - } -} - -impl AuthorizationSerializer for CodeFlowCredential { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { - self.serializer - .authority(azure_cloud_instance, &Authority::Common); - - if self.refresh_token.is_none() { - let uri = self.serializer.get(OAuthParameter::TokenUrl).ok_or( - AuthorizationFailure::msg_err("access_token_url", "Internal Error"), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) - } else { - let uri = self.serializer.get(OAuthParameter::RefreshTokenUrl).ok_or( - AuthorizationFailure::msg_err("refresh_token_url", "Internal Error"), - )?; - Url::parse(uri.as_str()).map_err(AuthorizationFailure::from) - } - } - - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - if self.client_id.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); - } - - if self.client_secret.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::ClientSecret.alias()); - } - - if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::RedirectUri); - } - - self.serializer - .client_id(self.client_id.as_str()) - .client_secret(self.client_secret.as_str()) - .redirect_uri(self.redirect_uri.as_str()) - .legacy_authority(); - - if let Some(refresh_token) = self.refresh_token.as_ref() { - if refresh_token.trim().is_empty() { - return AuthorizationFailure::msg_result( - OAuthParameter::RefreshToken.alias(), - "Either authorization code or refresh token is required", - ); - } - - self.serializer.refresh_token(refresh_token.as_ref()); - - return self.serializer.as_credential_map( - vec![], - vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::RefreshToken, - ], - ); - } else if let Some(authorization_code) = self.authorization_code.as_ref() { - if authorization_code.trim().is_empty() { - return AuthorizationFailure::msg_result( - OAuthParameter::RefreshToken.alias(), - "Either authorization code or refresh token is required", - ); - } - - self.serializer - .authorization_code(authorization_code.as_ref()); - - return self.serializer.as_credential_map( - vec![], - vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::AuthorizationCode, - ], - ); - } - - AuthorizationFailure::msg_result( - format!( - "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() - ), - "Either authorization code or refresh token is required", - ) - } -} - -#[derive(Clone, Eq, PartialEq)] -pub struct CodeFlowCredentialBuilder { - credential: CodeFlowCredential, -} - -impl CodeFlowCredentialBuilder { - fn new() -> CodeFlowCredentialBuilder { - CodeFlowCredentialBuilder { - credential: CodeFlowCredential { - authorization_code: None, - refresh_token: None, - client_id: String::new(), - client_secret: String::new(), - redirect_uri: String::new(), - serializer: Default::default(), - }, - } - } - - pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { - self.credential.refresh_token = None; - self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); - self - } - - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) -> &mut Self { - self.credential.authorization_code = None; - self.credential.refresh_token = Some(refresh_token.as_ref().to_owned()); - self - } - - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.credential.redirect_uri = redirect_uri.as_ref().to_owned(); - self - } - - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.client_id = client_id.as_ref().to_owned(); - self - } - - pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { - self.credential.client_secret = client_secret.as_ref().to_owned(); - self - } - - pub fn build(&self) -> CodeFlowCredential { - self.credential.clone() - } -} - -impl Default for CodeFlowCredentialBuilder { - fn default() -> Self { - CodeFlowCredentialBuilder::new() - } -} diff --git a/graph-oauth/src/identity/credentials/legacy/mod.rs b/graph-oauth/src/identity/credentials/legacy/mod.rs index 631ba03d..90c3bda5 100644 --- a/graph-oauth/src/identity/credentials/legacy/mod.rs +++ b/graph-oauth/src/identity/credentials/legacy/mod.rs @@ -1,9 +1,3 @@ -mod code_flow_authorization_url; -mod code_flow_credential; mod implicit_credential; -mod token_flow_authorization_url; -pub use code_flow_authorization_url::*; -pub use code_flow_credential::*; pub use implicit_credential::*; -pub use token_flow_authorization_url::*; diff --git a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs b/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs deleted file mode 100644 index 3137a520..00000000 --- a/graph-oauth/src/identity/credentials/legacy/token_flow_authorization_url.rs +++ /dev/null @@ -1,109 +0,0 @@ -use url::Url; - -use graph_error::{AuthorizationFailure, IdentityResult, AF}; - -use crate::auth::{OAuthParameter, OAuthSerializer}; -use crate::oauth::ResponseType; - -/// Legacy sign in for personal microsoft accounts to get access tokens for OneDrive -/// Not recommended - Instead use Microsoft Identity Platform OAuth 2.0 and OpenId Connect. -/// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/msa-oauth?view=odsp-graph-online#token-flow -#[derive(Clone)] -pub struct TokenFlowAuthorizationUrl { - pub(crate) client_id: String, - pub(crate) redirect_uri: String, - pub(crate) response_type: ResponseType, - pub(crate) scope: Vec<String>, -} - -impl TokenFlowAuthorizationUrl { - pub fn new<T: AsRef<str>, U: ToString, I: IntoIterator<Item = U>>( - client_id: T, - redirect_uri: T, - scope: I, - ) -> TokenFlowAuthorizationUrl { - TokenFlowAuthorizationUrl { - client_id: client_id.as_ref().to_owned(), - redirect_uri: redirect_uri.as_ref().to_owned(), - response_type: ResponseType::Token, - scope: scope.into_iter().map(|s| s.to_string()).collect(), - } - } - - pub fn builder() -> TokenFlowAuthorizationUrlBuilder { - TokenFlowAuthorizationUrlBuilder::new() - } - - pub fn url(&self) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); - if self.redirect_uri.trim().is_empty() { - return AuthorizationFailure::result("redirect_uri"); - } - - if self.client_id.trim().is_empty() { - return AuthorizationFailure::result("client_id"); - } - - if self.scope.is_empty() { - return AuthorizationFailure::result("scope"); - } - - serializer - .client_id(self.client_id.as_str()) - .redirect_uri(self.redirect_uri.as_str()) - .extend_scopes(self.scope.clone()) - .legacy_authority() - .response_type(self.response_type.clone()); - - let query = serializer.encode_query( - vec![], - vec![ - OAuthParameter::ClientId, - OAuthParameter::RedirectUri, - OAuthParameter::Scope, - OAuthParameter::ResponseType, - ], - )?; - - if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(query.as_str())); - Ok(url) - } else { - AF::msg_internal_result("authorization_url") - } - } -} - -#[derive(Clone)] -pub struct TokenFlowAuthorizationUrlBuilder { - authorization_url: TokenFlowAuthorizationUrl, -} - -impl TokenFlowAuthorizationUrlBuilder { - fn new() -> TokenFlowAuthorizationUrlBuilder { - TokenFlowAuthorizationUrlBuilder { - authorization_url: TokenFlowAuthorizationUrl { - client_id: String::new(), - redirect_uri: String::new(), - response_type: ResponseType::Token, - scope: vec![], - }, - } - } - - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.authorization_url.client_id = client_id.as_ref().to_owned(); - self - } - - pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.authorization_url.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self - } - - pub fn with_redirect_uri<T: AsRef<str>>(&mut self, redirect_uri: T) -> &mut Self { - self.authorization_url.redirect_uri = redirect_uri.as_ref().to_owned(); - self - } -} diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index 214f6358..a7d883ce 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -18,7 +18,8 @@ use uuid::Uuid; /// (e.g., clients executing on the device used by the resource owner, such as an /// installed native application or a web browser-based application), and incapable of /// secure client authentication via any other means. -/// https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 +/// +/// See [Client Types](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1) in the specification. #[derive(Clone, Debug)] pub struct PublicClientApplication<Credential> { credential: Credential, diff --git a/graph-oauth/src/identity/device_code.rs b/graph-oauth/src/identity/device_authorization_response.rs similarity index 59% rename from graph-oauth/src/identity/device_code.rs rename to graph-oauth/src/identity/device_authorization_response.rs index 2c6590bb..dcff4f7a 100644 --- a/graph-oauth/src/identity/device_code.rs +++ b/graph-oauth/src/identity/device_authorization_response.rs @@ -1,15 +1,24 @@ use std::collections::{BTreeSet, HashMap}; +use std::fmt::{Display, Formatter}; use std::str::FromStr; -use crate::identity::PublicClientApplication; -use crate::oauth::DeviceCodeCredential; -use crate::web::InteractiveAuthEvent; -use graph_core::http::JsonHttpResponse; use serde_json::Value; -/// https://datatracker.ietf.org/doc/html/rfc8628#section-3.2 -/// The actual device code response that is received from Microsoft Graph -/// looks similar to the following: +#[cfg(feature = "interactive-auth")] +use crate::web::{InteractiveAuthEvent, WindowCloseReason}; + +#[cfg(feature = "interactive-auth")] +use crate::identity::{DeviceCodeCredential, PublicClientApplication}; + +/// The Device Authorization Response: the authorization server generates a unique device +/// verification code and an end-user code that are valid for a limited time and includes +/// them in the HTTP response body using the "application/json" format [RFC8259] with a +/// 200 (OK) status code +/// +/// The actual [device code response](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code#device-authorization-response) +/// that is received from Microsoft Graph does not include the verification_uri_complete field +/// that the [specification](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2) +/// The device code response from Microsoft Graph looks like similar to the following: /// /// ```json /// { @@ -22,7 +31,7 @@ use serde_json::Value; /// } /// ``` #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct DeviceCode { +pub struct DeviceAuthorizationResponse { /// A long string used to verify the session between the client and the authorization server. /// The client uses this parameter to request the access token from the authorization server. pub device_code: String, @@ -36,16 +45,18 @@ pub struct DeviceCode { pub interval: u64, /// User friendly text response that can be used for display purpose. pub message: String, + /// A short string shown to the user that's used to identify the session on a secondary device. pub user_code: String, /// Verification URL where the user must navigate to authenticate using the device code /// and credentials. pub verification_uri: String, /// The verification_uri_complete response field is not included or supported - /// by Microsoft at this time. + /// by Microsoft at this time. It is included here because it is part of the + /// [standard](https://datatracker.ietf.org/doc/html/rfc8628) and in the case + /// that Microsoft decides to include it. pub verification_uri_complete: Option<String>, /// List of the scopes that would be held by token. pub scopes: Option<BTreeSet<String>>, - pub error: Option<String>, #[serde(flatten)] pub additional_fields: HashMap<String, Value>, } @@ -54,9 +65,26 @@ fn default_interval() -> u64 { 5 } +impl Display for DeviceAuthorizationResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}, {}, {}, {}, {}, {}, {:#?}, {:#?}", + self.device_code, + self.expires_in, + self.interval, + self.message, + self.user_code, + self.verification_uri, + self.verification_uri_complete, + self.scopes + ) + } +} + /// Response types used when polling for a device code /// https://datatracker.ietf.org/doc/html/rfc8628#section-3.5 -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum PollDeviceCodeEvent { /// The user hasn't finished authenticating, but hasn't canceled the flow. /// Repeat the request after at least interval seconds. @@ -85,6 +113,19 @@ pub enum PollDeviceCodeEvent { SlowDown, } +impl PollDeviceCodeEvent { + pub fn as_str(&self) -> &'static str { + match self { + PollDeviceCodeEvent::AuthorizationPending => "authorization_pending", + PollDeviceCodeEvent::AuthorizationDeclined => "authorization_declined", + PollDeviceCodeEvent::BadVerificationCode => "bad_verification_code", + PollDeviceCodeEvent::ExpiredToken => "expired_token", + PollDeviceCodeEvent::AccessDenied => "access_denied", + PollDeviceCodeEvent::SlowDown => "slow_down", + } + } +} + impl FromStr for PollDeviceCodeEvent { type Err = (); @@ -101,21 +142,30 @@ impl FromStr for PollDeviceCodeEvent { } } +impl AsRef<str> for PollDeviceCodeEvent { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for PollDeviceCodeEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[cfg(feature = "interactive-auth")] #[derive(Debug)] pub enum InteractiveDeviceCodeEvent { - BeginAuth { - response: JsonHttpResponse, - device_code: Option<DeviceCode>, - }, - FailedAuth { + DeviceAuthorizationResponse { response: JsonHttpResponse, - device_code: Option<DeviceCode>, + device_authorization_response: Option<DeviceAuthorizationResponse>, }, PollDeviceCode { poll_device_code_event: PollDeviceCodeEvent, response: JsonHttpResponse, }, - InteractiveAuthEvent(InteractiveAuthEvent), + WindowClosed(WindowCloseReason), SuccessfulAuthEvent { response: JsonHttpResponse, public_application: PublicClientApplication<DeviceCodeCredential>, diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 7f1f5c0e..fa886d55 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -1,3 +1,17 @@ +mod allowed_host_validator; +mod application_options; +mod authority; +mod authorization_query_response; +mod authorization_request_parts; +mod authorization_response; +mod authorization_serializer; +mod credentials; +mod device_authorization_response; + +mod id_token; +mod token; +mod token_validator; + #[cfg(feature = "openssl")] pub use openssl::{ pkey::{PKey, Private}, @@ -9,22 +23,10 @@ pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; pub use authorization_request_parts::*; +pub use authorization_response::*; pub use authorization_serializer::*; pub use credentials::*; -pub use device_code::*; +pub use device_authorization_response::*; pub use id_token::*; pub use token::*; pub use token_validator::*; - -mod allowed_host_validator; -mod application_options; -mod authority; -mod authorization_query_response; -mod authorization_request_parts; -mod authorization_serializer; -mod credentials; -mod device_code; - -mod id_token; -mod token; -mod token_validator; diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 105a9d20..d3eeab51 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -52,6 +52,8 @@ struct PhantomToken { /// Resources validate access tokens to grant access to a client application. /// For more information, see [Access tokens in the Microsoft Identity Platform](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// +/// For more info from the specification see [Successful Response](https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1) +/// /// Create a new AccessToken. /// # Example /// ``` @@ -67,6 +69,15 @@ struct PhantomToken { /// ``` #[derive(Clone, Eq, PartialEq, Serialize)] pub struct Token { + /// Access tokens are credentials used to access protected resources. An + /// access token is a string representing an authorization issued to the + /// client. The string is usually opaque to the client. Tokens + /// represent specific scopes and durations of access, granted by the + /// resource owner, and enforced by the resource server and authorization + /// server. + /// + /// See [Access Token](https://www.rfc-editor.org/rfc/rfc6749.html#section-1.4) in + /// the specification pub access_token: String, pub token_type: String, #[serde(deserialize_with = "deserialize_number_from_string")] @@ -77,6 +88,17 @@ pub struct Token { #[serde(deserialize_with = "deserialize_scope")] pub scope: Vec<String>, + /// Refresh tokens are credentials used to obtain access tokens. Refresh tokens are issued + /// to the client by the authorization server and are used to obtain a new access token + /// when the current access token becomes invalid or expires, or to obtain additional + /// access tokens with identical or narrower scope (access tokens may have a shorter + /// lifetime and fewer permissions than authorized by the resource owner). + /// Issuing a refresh token is optional at the discretion of the authorization server. + /// If the authorization server issues a refresh token, it is included when issuing an + /// access token + /// + /// See [Refresh Token](https://www.rfc-editor.org/rfc/rfc6749.html#section-1.5) in the specification + /// /// Because access tokens are valid for only a short period of time, /// authorization servers sometimes issue a refresh token at the same /// time the access token is issued. The client application can then diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index aa7dafea..c04b4519 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -53,7 +53,7 @@ mod oauth_error; pub(crate) mod identity; -//#[cfg(feature = "interactive-auth")] +#[cfg(feature = "interactive-auth")] pub(crate) mod web; pub(crate) mod internal { @@ -70,7 +70,7 @@ pub mod oauth { pub use crate::identity::*; - //#[cfg(feature = "interactive-auth")] + #[cfg(feature = "interactive-auth")] pub mod web { pub use crate::web::*; } diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 7661498b..4dfcb75f 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -238,12 +238,9 @@ impl InteractiveWebView { sender: std::sync::mpsc::Sender<InteractiveDeviceCodeEvent>, ) -> anyhow::Result<()> { tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); - //let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() .with_any_thread(true) .build(); - let proxy = event_loop.create_proxy(); - let sender2 = sender.clone(); let window = WindowBuilder::new() .with_title(options.window_title) @@ -262,11 +259,7 @@ impl InteractiveWebView { .with_file_drop_handler(|_, _| true) .with_navigation_handler(move |uri| { if let Ok(url) = Url::parse(uri.as_str()) { - sender2 - .send(InteractiveDeviceCodeEvent::InteractiveAuthEvent( - InteractiveAuthEvent::ReachedRedirectUri(url.clone()), - )) - .unwrap_or_default(); + tracing::event!(tracing::Level::INFO, url = url.as_str()); } true }) @@ -282,9 +275,12 @@ impl InteractiveWebView { match event { Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { - sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { - start, requested_resume - }))).unwrap_or_default(); + sender.send(InteractiveDeviceCodeEvent::WindowClosed( + WindowCloseReason::TimedOut { + start, requested_resume + } + )).unwrap_or_default(); + tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); if options.clear_browsing_data { @@ -299,9 +295,7 @@ impl InteractiveWebView { event: WindowEvent::CloseRequested, .. } => { - sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested))).unwrap_or_default(); - tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); - + sender.send(InteractiveDeviceCodeEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); } @@ -310,34 +304,6 @@ impl InteractiveWebView { std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } - Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { - tracing::error!(target: "interactive_webview", "WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); - if options.close_window_on_invalid_uri_navigation { - tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); - sender.send(InteractiveDeviceCodeEvent::InteractiveAuthEvent(InteractiveAuthEvent::WindowClosed(WindowCloseReason::InvalidWindowNavigation))).unwrap_or_default(); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_secs(1)); - - *control_flow = ControlFlow::Exit; - } - } _ => (), } }); diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index 4ec6429b..67c04a6f 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -1,5 +1,4 @@ -use std::ops::Add; -use std::time::{Duration, Instant}; +use std::time::Instant; pub use wry::application::window::Theme; diff --git a/src/client/graph.rs b/src/client/graph.rs index 4c75718a..529ba993 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -70,7 +70,7 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; -use graph_oauth::oauth::OpenIdCredential; +use graph_oauth::oauth::{DeviceCodeCredential, OpenIdCredential, PublicClientApplication}; use lazy_static::lazy_static; lazy_static! { @@ -596,6 +596,12 @@ impl From<&ConfidentialClientApplication<OpenIdCredential>> for Graph { } } +impl From<&PublicClientApplication<DeviceCodeCredential>> for Graph { + fn from(value: &PublicClientApplication<DeviceCodeCredential>) -> Self { + Graph::from_client_app(value.clone()) + } +} + #[cfg(test)] mod test { use super::*; From bcfd225eb2af3c0ef1e9e8bd2c2bf6724092a485 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:30:08 -0500 Subject: [PATCH 059/118] Fix errors with interactive-auth feature --- Cargo.toml | 1 + .../webview_errors.rs | 2 ++ .../credentials/application_builder.rs | 1 + .../auth_code_authorization_url.rs | 6 +++--- .../authorization_code_credential.rs | 21 +++++++++++++++++++ .../credentials/device_code_credential.rs | 12 +++++------ .../identity/device_authorization_response.rs | 5 ++++- graph-oauth/src/web/interactive_web_view.rs | 4 ++-- 8 files changed, 39 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 149eb976..bcb7dc42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.4" from_as = "0.2.0" +tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } graph-codegen = { path = "./graph-codegen", version = "0.0.1" } diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs index e90a3b8c..71b049e0 100644 --- a/examples/interactive_authentication/webview_errors.rs +++ b/examples/interactive_authentication/webview_errors.rs @@ -39,6 +39,8 @@ async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, r // This most likely came from an invalid parameter or missing parameter // passed to the client used for building the url. See graph_rs_sdk::oauth::AuthCodeAuthorizationUrlParameters WebViewExecutionError::AuthorizationError(authorization_failure) => {} + WebViewExecutionError::RecvError(_) => {} + WebViewExecutionError::AuthExecutionError(_) => {} } } } diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 7e9282e0..72eb01e2 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -343,6 +343,7 @@ mod test { use http::header::AUTHORIZATION; use http::HeaderValue; use url::Url; + use uuid::Uuid; use crate::identity::{AadAuthorityAudience, AzureCloudInstance}; diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 6e8e6ea6..3839e674 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -17,7 +17,7 @@ use crate::identity::{ }; #[cfg(feature = "interactive-auth")] -use graph_error::WebViewResult; +use graph_error::{WebViewExecutionError, WebViewResult}; #[cfg(feature = "interactive-auth")] use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResponse, Token}; @@ -194,7 +194,7 @@ impl AuthCodeAuthorizationUrlParameters { next = iter.next(); } - return match next { + match next { None => unreachable!(), Some(auth_event) => match auth_event { InteractiveAuthEvent::InvalidRedirectUri(reason) => { @@ -224,7 +224,7 @@ impl AuthCodeAuthorizationUrlParameters { } } }, - }; + } } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 968a568a..5c9a69bb 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -265,6 +265,27 @@ impl AuthorizationCodeCredentialBuilder { } } + pub(crate) fn new_with_token( + app_config: AppConfig, + token: Token, + ) -> AuthorizationCodeCredentialBuilder { + let cache_id = app_config.cache_id.clone(); + let mut token_cache = InMemoryCacheStore::new(); + token_cache.store(cache_id, token); + + Self { + credential: AuthorizationCodeCredential { + app_config, + authorization_code: None, + refresh_token: None, + client_secret: String::new(), + code_verifier: None, + serializer: OAuthSerializer::new(), + token_cache, + }, + } + } + pub(crate) fn new_with_auth_code( app_config: AppConfig, authorization_code: impl AsRef<str>, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4b728216..99185703 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -33,14 +33,11 @@ use crate::oauth::InteractiveDeviceCodeEvent; use graph_error::WebViewResult; #[cfg(feature = "interactive-auth")] -use crate::web::{IInteractiveWebView, InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use crate::web::{InteractiveWebView, WebViewOptions, WindowCloseReason}; #[cfg(feature = "interactive-auth")] use std::sync::mpsc::{Receiver, Sender}; -#[cfg(feature = "interactive-auth")] -use std::thread; - const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; credential_builder!( @@ -569,6 +566,7 @@ pub struct DeviceCodeInteractiveAuth { pub device_authorization_response: DeviceAuthorizationResponse, } +#[allow(dead_code)] #[cfg(feature = "interactive-auth")] impl DeviceCodeInteractiveAuth { pub(crate) fn new( @@ -589,7 +587,7 @@ impl DeviceCodeInteractiveAuth { let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - DeviceCodePollingExecutor::execute_interactive_loop(sender, executor); + DeviceCodeInteractiveAuth::execute_interactive_loop(sender, executor); }); Ok(receiver) @@ -631,7 +629,7 @@ impl DeviceCodeInteractiveAuth { break; } InteractiveDeviceCodeEvent::SuccessfulAuthEvent { - response, + response: _, public_application, } => { tracing::debug!(target: "device_code_polling_executor", "PublicApplication: {public_application:#?}"); @@ -681,7 +679,7 @@ impl DeviceCodeInteractiveAuth { let interval = Duration::from_secs(device_authorization_response.interval); credential.with_device_code(device_code); - let sender2 = sender.clone(); + let sender2 = sender; std::thread::spawn(move || { let mut should_slow_down = false; diff --git a/graph-oauth/src/identity/device_authorization_response.rs b/graph-oauth/src/identity/device_authorization_response.rs index dcff4f7a..95c8251b 100644 --- a/graph-oauth/src/identity/device_authorization_response.rs +++ b/graph-oauth/src/identity/device_authorization_response.rs @@ -5,7 +5,10 @@ use std::str::FromStr; use serde_json::Value; #[cfg(feature = "interactive-auth")] -use crate::web::{InteractiveAuthEvent, WindowCloseReason}; +use graph_core::http::JsonHttpResponse; + +#[cfg(feature = "interactive-auth")] +use crate::web::WindowCloseReason; #[cfg(feature = "interactive-auth")] use crate::identity::{DeviceCodeCredential, PublicClientApplication}; diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 4dfcb75f..07c2a729 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -162,7 +162,7 @@ impl InteractiveWebView { event_loop.run(move |event, _, control_flow| { if let Some(timeout) = options.timeout.as_ref() { - *control_flow = ControlFlow::WaitUntil(timeout.clone()); + *control_flow = ControlFlow::WaitUntil(*timeout); } else { *control_flow = ControlFlow::Wait; } @@ -267,7 +267,7 @@ impl InteractiveWebView { event_loop.run(move |event, _, control_flow| { if let Some(timeout) = options.timeout.as_ref() { - *control_flow = ControlFlow::WaitUntil(timeout.clone()); + *control_flow = ControlFlow::WaitUntil(*timeout); } else { *control_flow = ControlFlow::Wait; } From ccaaece3d9aec3c5efe7830dc549a726b48e3531 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 11 Nov 2023 03:34:46 -0500 Subject: [PATCH 060/118] Create interactive auth trait for custom impl --- .../webview_errors.rs | 16 +- .../webview_options.rs | 11 +- .../auth_code_authorization_url.rs | 154 ++++++++++++++++-- .../src/web/interactive_authenticator.rs | 135 ++++++++++++++- graph-oauth/src/web/interactive_web_view.rs | 52 +++--- graph-oauth/src/web/web_view_options.rs | 59 ++++--- 6 files changed, 332 insertions(+), 95 deletions(-) diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs index 71b049e0..1a2a945c 100644 --- a/examples/interactive_authentication/webview_errors.rs +++ b/examples/interactive_authentication/webview_errors.rs @@ -1,8 +1,6 @@ -use anyhow::Error; -use graph_error::WebViewExecutionError; -use graph_oauth::oauth::AuthorizationCodeCredential; +use graph_rs_sdk::{error::WebViewExecutionError, oauth::AuthorizationCodeCredential}; -async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { +async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { let mut credential_builder_result = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) @@ -14,17 +12,11 @@ async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, r // ... } else if let Err(err) = credential_builder_result { match err { + // WebView Window Closed Before Sign In and Redirect. + WebViewExecutionError::WindowClosed(reason) => {} // Issues with the redirect uri such as specifying localhost // but not providing a port in the WebViewOptions. WebViewExecutionError::InvalidRedirectUri(uri) => {} - // The user closed the webview window without logging in. - WebViewExecutionError::WindowClosedRequested => {} - // The user navigated to a url that was not the login url - // or a redirect url specified. Requires that WebViewOptions - // has the enforcement of invalid navigation enabled. - WebViewExecutionError::WindowClosedOnInvalidNavigation => {} - // The webview exited because of a timeout defined in the WebViewOptions. - WebViewExecutionError::WindowClosedOnTimeoutReached => {} // The host or domain provided or set for login is invalid. // This could be an internal error and most likely will never happen. WebViewExecutionError::InvalidStartUri { reason } => {} diff --git a/examples/interactive_authentication/webview_options.rs b/examples/interactive_authentication/webview_options.rs index cf2cf913..726926b5 100644 --- a/examples/interactive_authentication/webview_options.rs +++ b/examples/interactive_authentication/webview_options.rs @@ -1,4 +1,5 @@ use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use std::collections::HashSet; use std::ops::Add; use std::time::{Duration, Instant}; @@ -6,15 +7,9 @@ fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" .with_window_title("Sign In") - // OS specific theme. Does not work on all operating systems. + // OS specific theme. Windows only. // See wry crate for more info. .with_theme(Theme::Dark) - // Close the webview window whenever there is a navigation by the webview or user - // to a url that is not one of the redirect urls or the login url. - // For instance, if this is considered a security issue and the user should - // not be able to navigate to another url. - // Either way, the url bar does not show regardless. - .with_close_window_on_invalid_navigation(true) // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the // user to log in and the user has not logged in after 20 minutes you may @@ -27,7 +22,7 @@ fn get_webview_options() -> WebViewOptions { // Provide a list of ports to use for interactive authentication. // This assumes that you have http://localhost or http://localhost:port // for each port registered in your ADF application registration. - .with_ports(&[8000]) + .with_ports(HashSet::from([8000])) } async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 3839e674..025fa37e 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; +use std::sync::mpsc::Receiver; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; @@ -24,9 +25,18 @@ use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResp #[cfg(feature = "interactive-auth")] use crate::web::{ - InteractiveAuthEvent, InteractiveAuthenticator, WebViewOptions, WindowCloseReason, + HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, + WebViewOptions, WindowCloseReason, }; +#[cfg(feature = "interactive-auth")] +use wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, +}; + +use crate::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; + credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// Get the authorization url required to perform the initial authorization and redirect in the @@ -135,9 +145,9 @@ impl Debug for AuthCodeAuthorizationUrlParameters { } impl AuthCodeAuthorizationUrlParameters { - pub fn new<T: AsRef<str>, U: IntoUrl>( - client_id: T, - redirect_uri: U, + pub fn new( + client_id: impl AsRef<str>, + redirect_uri: impl IntoUrl, ) -> IdentityResult<AuthCodeAuthorizationUrlParameters> { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); @@ -186,7 +196,13 @@ impl AuthCodeAuthorizationUrlParameters { &self, interactive_web_view_options: Option<WebViewOptions>, ) -> WebViewResult<AuthorizationQueryResponse> { - let receiver = self.interactive_authentication(interactive_web_view_options)?; + let uri = self.url()?; + let redirect_uri = self.redirect_uri().cloned().unwrap(); + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let (sender, receiver) = std::sync::mpsc::channel(); + + self.interactive_auth(uri, vec![redirect_uri], web_view_options, sender) + .unwrap(); let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -207,24 +223,128 @@ impl AuthCodeAuthorizationUrlParameters { let response_query: AuthorizationQueryResponse = serde_urlencoded::from_str(query)?; + Ok(response_query) } - InteractiveAuthEvent::WindowClosed(window_close_reason) => { - match window_close_reason { - WindowCloseReason::CloseRequested => { - Err(WebViewExecutionError::WindowClosedRequested) - } - WindowCloseReason::InvalidWindowNavigation => { - Err(WebViewExecutionError::WindowClosedOnInvalidNavigation) - } - WindowCloseReason::TimedOut { - start: _, - requested_resume: _, - } => Err(WebViewExecutionError::WindowClosedOnTimeoutReached), + InteractiveAuthEvent::WindowClosed(window_close_reason) => Err( + WebViewExecutionError::WindowClosed(window_close_reason.to_string()), + ), + }, + } + } + + #[cfg(feature = "interactive-auth")] + pub fn interactive_authentication( + &self, + interactive_web_view_options: Option<WebViewOptions>, + ) -> WebViewResult<Receiver<AuthCodeInteractiveEvent>> { + let uri = self.url()?; + let redirect_uri = self.redirect_uri().cloned().unwrap(); + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let (sender, receiver) = std::sync::mpsc::channel(); + + self.interactive_auth(uri, vec![redirect_uri], web_view_options, sender) + .unwrap(); + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + + while next.is_none() { + next = iter.next(); + } + + let (event_sender, event_receiver) = std::sync::mpsc::channel(); + + match next { + None => unreachable!(), + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + return Err(WebViewExecutionError::InvalidRedirectUri(reason)); + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let query = uri.query().or(uri.fragment()).ok_or( + WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), + )?; + + let response_query: AuthorizationQueryResponse = + serde_urlencoded::from_str(query)?; + + event_sender + .send(AuthCodeInteractiveEvent::AuthorizationQuery(Box::new( + response_query.clone(), + ))) + .unwrap_or_default(); + + if let Some(code) = response_query.code.as_ref() { + let credential = AuthorizationCodeCredentialBuilder::new_with_auth_code( + self.app_config.clone(), + code, + ) + .build(); + event_sender + .send(AuthCodeInteractiveEvent::Success(credential)) + .unwrap_or_default(); } } + InteractiveAuthEvent::WindowClosed(window_close_reason) => { + event_sender + .send(AuthCodeInteractiveEvent::WindowClosed(window_close_reason)) + .unwrap_or_default(); + } }, } + + Ok(event_receiver) + } +} + +#[cfg(feature = "interactive-auth")] +#[derive(Debug)] +pub enum AuthCodeInteractiveEvent { + AuthorizationQuery(Box<AuthorizationQueryResponse>), + WindowClosed(WindowCloseReason), + Success(ConfidentialClientApplication<AuthorizationCodeCredential>), +} + +#[cfg(feature = "interactive-auth")] +impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { + fn webview( + &self, + host_options: HostOptions, + _options: WebViewOptions, + window: Window, + proxy: EventLoopProxy<UserEvents>, + sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, + ) -> anyhow::Result<WebView> { + let start_uri = host_options.start_uri.clone(); + let validator = WebViewHostValidator::try_from(host_options)?; + Ok(WebViewBuilder::new(window)? + .with_url(start_uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + let is_valid_host = validator.is_valid_uri(&url); + let is_redirect = validator.is_redirect_host(&url); + + if is_redirect { + sender + .send(InteractiveAuthEvent::ReachedRedirectUri(url.clone())) + .unwrap_or_default(); + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + + let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); + return true; + } + + is_valid_host + } else { + tracing::debug!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); + let _ = proxy.send_event(UserEvents::CloseWindow); + false + } + }) + .build()?) } } diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index 21186d5e..b8d5fbf4 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -1,7 +1,14 @@ -use crate::web::WebViewOptions; +use crate::web::{HostOptions, UserEvents, WebViewOptions}; use graph_error::WebViewResult; -use std::time::Instant; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::mpsc::Sender; +use std::time::{Duration, Instant}; use url::Url; +use wry::application::event::{Event, StartCause, WindowEvent}; +use wry::application::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; +use wry::application::platform::windows::EventLoopBuilderExtWindows; +use wry::application::window::{Window, WindowBuilder}; +use wry::webview::WebView; pub trait InteractiveAuthenticator { fn interactive_authentication( @@ -10,16 +17,138 @@ pub trait InteractiveAuthenticator { ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; } +pub trait InteractiveAuth +where + Self: Debug, +{ + fn webview( + &self, + host_options: HostOptions, + options: WebViewOptions, + window: Window, + proxy: EventLoopProxy<UserEvents>, + sender: Sender<InteractiveAuthEvent>, + ) -> anyhow::Result<WebView>; + + #[tracing::instrument] + fn interactive_auth( + &self, + start_url: Url, + redirect_uris: Vec<Url>, + options: WebViewOptions, + sender: Sender<InteractiveAuthEvent>, + ) -> anyhow::Result<()> { + let event_loop: EventLoop<UserEvents> = Self::event_loop(); + let proxy = event_loop.create_proxy(); + let window = Self::window_builder(&options).build(&event_loop).unwrap(); + + let webview_options = options.clone(); + let webview = self.webview( + HostOptions::new(start_url, redirect_uris, options.ports.clone()), + webview_options, + window, + proxy, + sender.clone(), + )?; + + event_loop.run(move |event, _, control_flow| { + if let Some(timeout) = options.timeout.as_ref() { + *control_flow = ControlFlow::WaitUntil(*timeout); + } else { + *control_flow = ControlFlow::Wait; + } + + match event { + Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), + Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { + sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { + start, requested_resume + })).unwrap_or_default(); + tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); + tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + _ => (), + } + }); + } + + fn window_builder(options: &WebViewOptions) -> WindowBuilder { + WindowBuilder::new() + .with_title(options.window_title.clone()) + .with_closable(true) + .with_content_protection(true) + .with_minimizable(true) + .with_maximizable(true) + .with_focused(true) + .with_resizable(true) + .with_theme(options.theme) + } + + #[cfg(target_family = "windows")] + fn event_loop() -> EventLoop<UserEvents> { + EventLoopBuilder::with_user_event() + .with_any_thread(true) + .build() + } + + #[cfg(target_family = "unix")] + fn event_loop() -> EventLoop<UserEvents> { + EventLoopBuilder::with_user_event().build() + } +} + #[derive(Clone, Debug)] pub enum WindowCloseReason { CloseRequested, - InvalidWindowNavigation, TimedOut { start: Instant, requested_resume: Instant, }, } +impl Display for WindowCloseReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + WindowCloseReason::CloseRequested => write!(f, "CloseRequested"), + WindowCloseReason::TimedOut { .. } => write!(f, "TimedOut"), + } + } +} + #[derive(Clone, Debug)] pub enum InteractiveAuthEvent { InvalidRedirectUri(String), diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index 07c2a729..cd863e5a 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -1,8 +1,9 @@ +use std::collections::HashSet; use std::time::Duration; use url::Url; use crate::oauth::InteractiveDeviceCodeEvent; -use crate::web::{InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; +use crate::web::{HostOptions, InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; use graph_error::{WebViewExecutionError, WebViewResult}; use wry::application::event_loop::EventLoopBuilder; use wry::application::platform::windows::EventLoopBuilderExtWindows; @@ -19,22 +20,21 @@ use wry::{ pub enum UserEvents { CloseWindow, ReachedRedirectUri(Url), - InvalidNavigationAttempt(Option<Url>), } -struct WebViewValidHosts { +pub(crate) struct WebViewHostValidator { start_uri: Url, redirect_uris: Vec<Url>, - ports: Vec<usize>, + ports: HashSet<usize>, is_local_host: bool, } -impl WebViewValidHosts { - fn new( +impl WebViewHostValidator { + pub fn new( start_uri: Url, redirect_uris: Vec<Url>, - ports: Vec<usize>, - ) -> WebViewResult<WebViewValidHosts> { + ports: HashSet<usize>, + ) -> WebViewResult<WebViewHostValidator> { if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { return Err(WebViewExecutionError::InvalidStartUri { reason: "Authorization url and redirect uri must have valid uri hosts".to_owned(), @@ -51,7 +51,7 @@ impl WebViewValidHosts { }); } - Ok(WebViewValidHosts { + Ok(WebViewHostValidator { start_uri, redirect_uris, ports, @@ -59,7 +59,7 @@ impl WebViewValidHosts { }) } - fn is_valid_uri(&self, url: &Url) -> bool { + pub fn is_valid_uri(&self, url: &Url) -> bool { if let Some(host) = url.host() { if self.is_local_host && !self.ports.is_empty() { let hosts: Vec<url::Host> = self @@ -87,7 +87,7 @@ impl WebViewValidHosts { } } - fn is_redirect_host(&self, url: &Url) -> bool { + pub fn is_redirect_host(&self, url: &Url) -> bool { if let Some(host) = url.host() { self.redirect_uris .iter() @@ -98,6 +98,14 @@ impl WebViewValidHosts { } } +impl TryFrom<HostOptions> for WebViewHostValidator { + type Error = WebViewExecutionError; + + fn try_from(value: HostOptions) -> Result<Self, Self::Error> { + WebViewHostValidator::new(value.start_uri, value.redirect_uris, value.ports) + } +} + pub struct InteractiveWebView; impl InteractiveWebView { @@ -109,7 +117,7 @@ impl InteractiveWebView { sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, ) -> anyhow::Result<()> { tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); - let validator = WebViewValidHosts::new(uri.clone(), redirect_uris, options.ports)?; + let validator = WebViewHostValidator::new(uri.clone(), redirect_uris, options.ports)?; let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() .with_any_thread(true) .build(); @@ -147,10 +155,6 @@ impl InteractiveWebView { return true; } - if !is_valid_host { - let _ = proxy.send_event(UserEvents::InvalidNavigationAttempt(Some(url))); - } - is_valid_host } else { tracing::debug!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); @@ -210,22 +214,6 @@ impl InteractiveWebView { std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } - Event::UserEvent(UserEvents::InvalidNavigationAttempt(uri_option)) => { - tracing::error!(target: "interactive_webview", "WebView attempted to navigate to invalid host with uri: {uri_option:#?}"); - if options.close_window_on_invalid_uri_navigation { - tracing::error!(target: "interactive_webview", "Closing window due to attempted navigation to invalid host with uri: {uri_option:#?}"); - sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::InvalidWindowNavigation)).unwrap_or_default(); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_secs(1)); - - *control_flow = ControlFlow::Exit; - } - } _ => (), } }); diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index 67c04a6f..491eeca7 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -1,24 +1,47 @@ +use std::collections::HashSet; use std::time::Instant; +use url::Url; pub use wry::application::window::Theme; +pub struct HostOptions { + pub(crate) start_uri: Url, + pub(crate) redirect_uris: Vec<Url>, + pub(crate) ports: HashSet<usize>, +} + +impl HostOptions { + pub fn new(start_uri: Url, redirect_uris: Vec<Url>, ports: HashSet<usize>) -> HostOptions { + HostOptions { + start_uri, + redirect_uris, + ports, + } + } +} + +impl Default for HostOptions { + fn default() -> Self { + HostOptions { + start_uri: Url::parse("http://localhost").expect("Internal Error"), + redirect_uris: vec![], + ports: vec![3000].into_iter().collect(), + } + } +} + #[derive(Clone, Debug)] pub struct WebViewOptions { /// Give the window a title. The default is "Sign In" pub window_title: String, - /// Close the webview window whenever there is a navigation by the webview or user - /// to a url that is not one of the redirect urls or the login url. - /// For instance, if this is considered a security issue and the user should - /// not be able to navigate to another url. - /// Either way, the url bar does not show regardless. - pub close_window_on_invalid_uri_navigation: bool, - /// OS specific theme. Does not work on all operating systems. + /// OS specific theme. Only available on Windows. /// See wry crate for more info. + #[cfg(windows)] pub theme: Option<Theme>, /// Provide a list of ports to use for interactive authentication. /// This assumes that you have http://localhost or http://localhost:port /// for each port registered in your ADF application registration. - pub ports: Vec<usize>, + pub ports: HashSet<usize>, /// Add a timeout that will close the window and return an error /// when that timeout is reached. For instance, if your app is waiting on the /// user to log in and the user has not logged in after 20 minutes you may @@ -41,25 +64,16 @@ impl WebViewOptions { self } - /// Close the webview window whenever there is a navigation by the webview or user - /// to a url that is not one of the redirect urls or the login url. - /// For instance, if this is considered a security issue and the user should - /// not be able to navigate to another url. - /// Either way, the url bar does not show regardless. - pub fn with_close_window_on_invalid_navigation(mut self, close_window: bool) -> Self { - self.close_window_on_invalid_uri_navigation = close_window; - self - } - - /// OS specific theme. Does not work on all operating systems. + /// OS specific theme. Only available on Windows. /// See wry crate for more info. + #[cfg(windows)] pub fn with_theme(mut self, theme: Theme) -> Self { self.theme = Some(theme); self } - pub fn with_ports(mut self, ports: &[usize]) -> Self { - self.ports = ports.to_vec(); + pub fn with_ports(mut self, ports: HashSet<usize>) -> Self { + self.ports = ports; self } @@ -85,9 +99,8 @@ impl Default for WebViewOptions { fn default() -> Self { WebViewOptions { window_title: "Sign In".to_string(), - close_window_on_invalid_uri_navigation: true, theme: None, - ports: vec![], + ports: Default::default(), // 10 Minutes default timeout timeout: None, clear_browsing_data: false, From 2844be19324a710a7f506a263394d949ec3b84fb Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 11 Nov 2023 23:15:53 -0500 Subject: [PATCH 061/118] Update Graph name to GraphClient and set tests to use app registrations only --- examples/batch.rs | 2 +- examples/blocking.rs | 10 +-- examples/client_configuration.rs | 6 +- examples/custom_request.rs | 4 +- examples/drive/check_in_out.rs | 4 +- examples/drive/copy.rs | 2 +- examples/drive/create_folder.rs | 2 +- examples/drive/delete.rs | 4 +- examples/drive/download.rs | 14 ++-- examples/drive/get_item.rs | 4 +- examples/drive/list_drive_items.rs | 6 +- examples/drive/preview_item.rs | 2 +- examples/drive/thumbnails.rs | 2 +- examples/drive/update_item.rs | 2 +- examples/drive/upload_and_update_file.rs | 8 +- examples/drive/upload_file.rs | 16 ++-- examples/groups/create_update_groups.rs | 4 +- examples/groups/get_groups.rs | 4 +- examples/groups/group_lifecycle_policies.rs | 14 ++-- examples/interactive_authentication/README.md | 2 +- .../interactive_authentication/webview.rs | 4 +- .../mail_folders_and_messages/attachments.rs | 8 +- .../child_folders.rs | 4 +- .../mail_folders_and_messages/mail_folders.rs | 12 +-- .../mailbox_settings.rs | 4 +- .../mail_folders_and_messages/messages.rs | 12 +-- examples/oauth/README.md | 2 +- .../auth_code_grant/auth_code_grant_secret.rs | 2 +- .../server_examples/auth_code_grant_secret.rs | 2 +- .../client_credentials_secret.rs | 6 +- examples/oauth/main.rs | 6 +- examples/oauth/openid/openid.rs | 6 +- examples/odata_query.rs | 20 ++--- examples/onenote/delete_page.rs | 2 +- examples/onenote/get_page_content.rs | 4 +- examples/onenote/upload_page_content.rs | 4 +- examples/paging/channel.rs | 2 +- examples/paging/delta.rs | 6 +- examples/paging/stream.rs | 2 +- examples/paging_and_next_links.rs | 2 +- examples/request_body_helper.rs | 6 +- examples/sites/get_sites.rs | 4 +- examples/sites/lists_items.rs | 12 +-- examples/teams/create_team.rs | 2 +- examples/teams/get_teams.rs | 4 +- .../upload_session/cancel_upload_session.rs | 2 +- .../upload_session/channel_upload_session.rs | 4 +- .../upload_session/stream_upload_session.rs | 2 +- .../upload_session/upload_bytes_iterator.rs | 4 +- .../upload_session/upload_file_iterator.rs | 4 +- examples/users/todos/tasks.rs | 8 +- examples/users/user.rs | 12 +-- graph-error/src/webview_error.rs | 13 +-- src/client/graph.rs | 79 ++++++++++--------- src/lib.rs | 2 +- test-tools/src/oauth_request.rs | 31 +++++--- 56 files changed, 206 insertions(+), 205 deletions(-) diff --git a/examples/batch.rs b/examples/batch.rs index 6b8bc370..cacea9ae 100644 --- a/examples/batch.rs +++ b/examples/batch.rs @@ -13,7 +13,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; #[tokio::main] async fn main() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let json = serde_json::json!({ "requests": [ { diff --git a/examples/blocking.rs b/examples/blocking.rs index 85715416..afc40d93 100644 --- a/examples/blocking.rs +++ b/examples/blocking.rs @@ -6,7 +6,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; fn main() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.users().list_user().into_blocking().send()?; @@ -19,7 +19,7 @@ fn main() -> GraphResult<()> { } fn paging_json() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .users() @@ -34,7 +34,7 @@ fn paging_json() -> GraphResult<()> { } fn paging_channel() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let receiver = client .users() @@ -61,7 +61,7 @@ static ONEDRIVE_FILE: &str = ":/file.txt:"; static LOCAL_FILE: &str = "./file.txt"; fn upload_session_channel() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some("fail".to_string()) @@ -94,7 +94,7 @@ fn upload_session_channel() -> GraphResult<()> { // Best way to use Iterator impl: fn upload_session_iter() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some("fail".to_string()) diff --git a/examples/client_configuration.rs b/examples/client_configuration.rs index 8ea7f0dc..bc1675b6 100644 --- a/examples/client_configuration.rs +++ b/examples/client_configuration.rs @@ -1,5 +1,5 @@ #![allow(dead_code, unused, unused_imports, clippy::module_inception)] -use graph_rs_sdk::{header::HeaderMap, header::HeaderValue, Graph, GraphClientConfiguration}; +use graph_rs_sdk::{header::HeaderMap, header::HeaderValue, GraphClient, GraphClientConfiguration}; use http::header::ACCEPT; use http::HeaderName; use std::time::Duration; @@ -12,13 +12,13 @@ fn main() { .timeout(Duration::from_secs(30)) .default_headers(HeaderMap::default()); - let _ = Graph::from(client_config); + let _ = GraphClient::from(client_config); } // Custom headers async fn per_request_headers() { - let client = Graph::new("token"); + let client = GraphClient::new("token"); let _result = client .users() diff --git a/examples/custom_request.rs b/examples/custom_request.rs index 305da609..b3526347 100644 --- a/examples/custom_request.rs +++ b/examples/custom_request.rs @@ -12,7 +12,7 @@ static USER_ID: &str = "USER_ID"; #[tokio::main] async fn main() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let user = serde_json::json!({ "business_phones": ["888-888-8888"] @@ -34,7 +34,7 @@ async fn main() -> GraphResult<()> { } async fn list_users() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .custom(Method::GET, None) diff --git a/examples/drive/check_in_out.rs b/examples/drive/check_in_out.rs index 1d90e772..2b1882b1 100644 --- a/examples/drive/check_in_out.rs +++ b/examples/drive/check_in_out.rs @@ -12,7 +12,7 @@ static ITEM_ID: &str = "ITEM_ID"; // For more information on checking out a drive item see: // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_checkout?view=odsp-graph-online async fn check_out_item() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -31,7 +31,7 @@ async fn check_out_item() { // For more information on checking in a drive item see: // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_checkin?view=odsp-graph-online async fn check_in_item() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); // checkInAs: Optional. The desired status of the document after the check-in // operation is complete. Can be 'published' or 'unspecified'. diff --git a/examples/drive/copy.rs b/examples/drive/copy.rs index b7398d05..e32cf3ed 100644 --- a/examples/drive/copy.rs +++ b/examples/drive/copy.rs @@ -14,7 +14,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static ITEM_ID: &str = "ITEM_ID"; pub async fn copy_item() { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); // The DriveItem copy request uses a ItemReference (parent reference) which contains // the metadata for the drive id and path specifying where the new copy should be placed. diff --git a/examples/drive/create_folder.rs b/examples/drive/create_folder.rs index f2a1fb5c..9038b0b2 100644 --- a/examples/drive/create_folder.rs +++ b/examples/drive/create_folder.rs @@ -9,7 +9,7 @@ static PARENT_ID: &str = "PARENT_ID"; // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online pub async fn create_new_folder() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let folder: HashMap<String, serde_json::Value> = HashMap::new(); let response = client diff --git a/examples/drive/delete.rs b/examples/drive/delete.rs index 67764d8a..5e120c20 100644 --- a/examples/drive/delete.rs +++ b/examples/drive/delete.rs @@ -8,7 +8,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; // Delete a drive item by id. pub async fn delete_by_id(item_id: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); // Send the request. let response = client @@ -26,7 +26,7 @@ pub async fn delete_by_id(item_id: &str) { // Deleting an item by path. pub async fn delete_by_path(path: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); // Send the request. let response = client diff --git a/examples/drive/download.rs b/examples/drive/download.rs index 474fdacf..c02b432c 100644 --- a/examples/drive/download.rs +++ b/examples/drive/download.rs @@ -20,7 +20,7 @@ pub async fn download_files() { } pub async fn download() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -43,7 +43,7 @@ pub async fn download() { } pub async fn download_file_as_bytes() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -59,7 +59,7 @@ pub async fn download_file_as_bytes() { } pub async fn download_file_as_string() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -82,7 +82,7 @@ pub async fn download_file_as_string() { // For more info on download formats see: // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get_content_format?view=odsp-graph-online pub async fn download_and_format(format: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -110,7 +110,7 @@ pub async fn download_and_format(format: &str) { } pub async fn download_and_rename(name: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -139,7 +139,7 @@ pub async fn download_and_rename(name: &str) { // The path should always start with :/ and end with : // such as :/Documents/item.txt: pub async fn download_by_path(path: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -169,7 +169,7 @@ pub async fn download_by_path(path: &str) { /// You can change this by setting FileConfig with create directories to true. /// Any missing directory when this is not true will cause the request to fail. pub async fn download_with_config() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/drive/get_item.rs b/examples/drive/get_item.rs index 87760c96..a937dfbd 100644 --- a/examples/drive/get_item.rs +++ b/examples/drive/get_item.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::*; pub async fn get_drive_item(item_id: &str) { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .me() @@ -22,7 +22,7 @@ pub async fn get_drive_item(item_id: &str) { // such as drives, users, groups, and sites. // The resource_id is the id for this location (sites, users, etc). pub async fn get_sites_drive_item(item_id: &str, sites_id: &str) { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .site(sites_id) diff --git a/examples/drive/list_drive_items.rs b/examples/drive/list_drive_items.rs index fdaf2dd9..36a3cc76 100644 --- a/examples/drive/list_drive_items.rs +++ b/examples/drive/list_drive_items.rs @@ -9,7 +9,7 @@ pub async fn list_drive_items() { } pub async fn drive_root() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.me().drive().get_root().send().await.unwrap(); @@ -20,7 +20,7 @@ pub async fn drive_root() { } pub async fn drive_root_children() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -37,7 +37,7 @@ pub async fn drive_root_children() { } pub async fn special_docs() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/drive/preview_item.rs b/examples/drive/preview_item.rs index 58f8cd1d..6697636a 100644 --- a/examples/drive/preview_item.rs +++ b/examples/drive/preview_item.rs @@ -12,7 +12,7 @@ static ONEDRIVE_FILE_PATH: &str = ":/Documents/file.txt:"; // zoom number Optional. Zoom level to start at, if applicable. pub async fn get_drive_item(item_id: &str) { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let body = serde_json::json!({ "viewer": null, diff --git a/examples/drive/thumbnails.rs b/examples/drive/thumbnails.rs index 5b19e54b..664acdba 100644 --- a/examples/drive/thumbnails.rs +++ b/examples/drive/thumbnails.rs @@ -3,7 +3,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; pub async fn list_thumbnails() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/drive/update_item.rs b/examples/drive/update_item.rs index c3e02897..0ab12dec 100644 --- a/examples/drive/update_item.rs +++ b/examples/drive/update_item.rs @@ -14,7 +14,7 @@ async fn update() { // Fields that are not included will not be changed. let value = serde_json::json!({ "name": DRIVE_FILE_NEW_NAME }); - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/drive/upload_and_update_file.rs b/examples/drive/upload_and_update_file.rs index 1adcc9ce..26a71c2a 100644 --- a/examples/drive/upload_and_update_file.rs +++ b/examples/drive/upload_and_update_file.rs @@ -35,7 +35,7 @@ async fn upload_new_file( parent_reference_id: &str, file_name: &str, local_file: &str, - client: &Graph, + client: &GraphClient, ) -> GraphResult<reqwest::Response> { client .drive(user_id) @@ -49,7 +49,7 @@ async fn update_file( user_id: &str, onedrive_file_path: &str, local_file: &str, - client: &Graph, + client: &GraphClient, ) -> GraphResult<reqwest::Response> { client .user(user_id) @@ -63,7 +63,7 @@ async fn update_file( async fn delete_file( user_id: &str, item_id: &str, - client: &Graph, + client: &GraphClient, ) -> GraphResult<reqwest::Response> { client .user(user_id) @@ -75,7 +75,7 @@ async fn delete_file( } async fn upload_and_update_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); // Get the id for the Documents folder where the file will be uploaded. let parent_reference_id = diff --git a/examples/drive/upload_file.rs b/examples/drive/upload_file.rs index ca12d787..b54b1438 100644 --- a/examples/drive/upload_file.rs +++ b/examples/drive/upload_file.rs @@ -25,7 +25,7 @@ static RESOURCE_ID: &str = "RESOURCE_ID"; // Uploading a file using the drive id and parent id. async fn upload_file() -> GraphResult<()> { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); let response = graph .me() .drive() @@ -44,7 +44,7 @@ async fn upload_file() -> GraphResult<()> { // Uploading a file using the drive id and parent id. async fn upload_file_reqwest_body() -> GraphResult<()> { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); let file = tokio::fs::File::open(LOCAL_FILE_PATH).await?; let body = reqwest::Body::from(file); @@ -66,7 +66,7 @@ async fn upload_file_reqwest_body() -> GraphResult<()> { } async fn upload_using_read() -> GraphResult<()> { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); let file = OpenOptions::new().read(true).open(LOCAL_FILE_PATH)?; @@ -88,7 +88,7 @@ async fn upload_using_read() -> GraphResult<()> { } async fn upload_using_async_read() -> GraphResult<()> { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); let file = tokio::fs::File::open(LOCAL_FILE_PATH).await?; let reader = BodyRead::from_async_read(file).await?; @@ -110,7 +110,7 @@ async fn upload_using_async_read() -> GraphResult<()> { } async fn upload_file_bytes_mut(bytes_mut: BytesMut) -> GraphResult<()> { - let graph = Graph::new(ACCESS_TOKEN); + let graph = GraphClient::new(ACCESS_TOKEN); let reader = BodyRead::try_from(bytes_mut)?; let response = graph @@ -132,7 +132,7 @@ async fn upload_file_bytes_mut(bytes_mut: BytesMut) -> GraphResult<()> { // Upload a file using a ParentReference. // This example uses the Documents folder of a users OneDrive. async fn drive_upload() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .drive(RESOURCE_ID) @@ -152,7 +152,7 @@ async fn drive_upload() -> GraphResult<()> { // Upload a file using a ParentReference. // This example uses the Documents folder of a users OneDrive. async fn user_upload() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(RESOURCE_ID) @@ -174,7 +174,7 @@ async fn user_upload() -> GraphResult<()> { // This example uses the Documents folder of a users OneDrive. async fn sites_upload() -> GraphResult<()> { // Get the latest metadata for the root drive folder items. - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(RESOURCE_ID) diff --git a/examples/groups/create_update_groups.rs b/examples/groups/create_update_groups.rs index f645b10a..130294d6 100644 --- a/examples/groups/create_update_groups.rs +++ b/examples/groups/create_update_groups.rs @@ -5,7 +5,7 @@ static ACCESS_TOKEN: &str = "<ACCESS_TOKEN>"; static GROUP_ID: &str = "<GROUP_ID>"; pub async fn create_group() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .groups() @@ -31,7 +31,7 @@ pub async fn create_group() -> GraphResult<()> { } pub async fn update_group() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group(GROUP_ID) diff --git a/examples/groups/get_groups.rs b/examples/groups/get_groups.rs index 6ab6899f..2484783d 100644 --- a/examples/groups/get_groups.rs +++ b/examples/groups/get_groups.rs @@ -5,7 +5,7 @@ static ACCESS_TOKEN: &str = "<ACCESS_TOKEN>"; static GROUP_ID: &str = "<GROUP_ID>"; pub async fn get_groups() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.group(GROUP_ID).get_group().send().await?; @@ -15,7 +15,7 @@ pub async fn get_groups() -> GraphResult<()> { } pub async fn list_groups() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.groups().list_group().send().await.unwrap(); diff --git a/examples/groups/group_lifecycle_policies.rs b/examples/groups/group_lifecycle_policies.rs index 95b9a3ed..8511a61e 100644 --- a/examples/groups/group_lifecycle_policies.rs +++ b/examples/groups/group_lifecycle_policies.rs @@ -5,7 +5,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static GROUP_LIFECYCLE_POLICY_ID: &str = "GROUP_LIFECYCLE_POLICY_ID"; pub async fn list_group_lifecycle_policies() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policies() @@ -24,7 +24,7 @@ pub async fn list_group_lifecycle_policies() -> GraphResult<()> { static GROUP_ID: &str = "<GROUP_ID>"; pub async fn list_group_lifecycle_policies_as_group() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group(GROUP_ID) @@ -42,7 +42,7 @@ pub async fn list_group_lifecycle_policies_as_group() -> GraphResult<()> { } pub async fn get_group_lifecycle_policies() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policy(GROUP_LIFECYCLE_POLICY_ID) @@ -59,7 +59,7 @@ pub async fn get_group_lifecycle_policies() -> GraphResult<()> { } pub async fn create_group_lifecycle_policies() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policies() @@ -77,7 +77,7 @@ pub async fn create_group_lifecycle_policies() -> GraphResult<()> { } pub async fn update_group_lifecycle_policies() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policy(GROUP_LIFECYCLE_POLICY_ID) @@ -95,7 +95,7 @@ pub async fn update_group_lifecycle_policies() -> GraphResult<()> { } pub async fn add_group() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policy(GROUP_LIFECYCLE_POLICY_ID) @@ -114,7 +114,7 @@ pub async fn add_group() -> GraphResult<()> { } pub async fn remove_group() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .group_lifecycle_policy(GROUP_LIFECYCLE_POLICY_ID) diff --git a/examples/interactive_authentication/README.md b/examples/interactive_authentication/README.md index 87f6892f..87b544a9 100644 --- a/examples/interactive_authentication/README.md +++ b/examples/interactive_authentication/README.md @@ -52,7 +52,7 @@ async fn authenticate() { let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); - let client = Graph::from(&confidential_client); + let client = GraphClient::from(&confidential_client); let response = client.user(USER_ID).get_user().send().await.unwrap(); diff --git a/examples/interactive_authentication/webview.rs b/examples/interactive_authentication/webview.rs index 2506cfe9..bffa6873 100644 --- a/examples/interactive_authentication/webview.rs +++ b/examples/interactive_authentication/webview.rs @@ -1,7 +1,7 @@ use graph_rs_sdk::oauth::{ web::Theme, web::WebViewOptions, AuthorizationCodeCredential, TokenCredentialExecutor, }; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; use std::ops::Add; use std::time::{Duration, Instant}; @@ -51,7 +51,7 @@ async fn authenticate() { let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); - let client = Graph::from(&confidential_client); + let client = GraphClient::from(&confidential_client); let response = client.user(USER_ID).get_user().send().await.unwrap(); diff --git a/examples/mail_folders_and_messages/attachments.rs b/examples/mail_folders_and_messages/attachments.rs index 7df8d95a..2aea86fe 100644 --- a/examples/mail_folders_and_messages/attachments.rs +++ b/examples/mail_folders_and_messages/attachments.rs @@ -10,7 +10,7 @@ static ATTACHMENT_ID: &str = "ATTACHMENT_ID"; static USER_ID: &str = "USER_ID"; pub async fn add_attachment() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -29,7 +29,7 @@ pub async fn add_attachment() { } pub async fn get_attachment() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -47,7 +47,7 @@ pub async fn get_attachment() { } pub async fn get_attachment_content() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -67,7 +67,7 @@ pub async fn get_attachment_content() { static MAIL_FOLDER_ID: &str = "MAIL_FOLDER_ID"; pub async fn add_mail_folder_message_attachment() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/mail_folders_and_messages/child_folders.rs b/examples/mail_folders_and_messages/child_folders.rs index c94ebf4f..e99c7e4f 100644 --- a/examples/mail_folders_and_messages/child_folders.rs +++ b/examples/mail_folders_and_messages/child_folders.rs @@ -13,7 +13,7 @@ static CHILD_FOLDER_ID_1: &str = "CHILD_FOLDER_ID1"; static CHILD_FOLDER_ID_2: &str = "CHILD_FOLDER_ID1"; pub async fn get_child_folders_attachment() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -35,7 +35,7 @@ pub async fn get_child_folders_attachment() { // You can keep calling the child_folder("id") method // until you get to the child folder you want. pub async fn get_child_folders_attachment_content() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/mail_folders_and_messages/mail_folders.rs b/examples/mail_folders_and_messages/mail_folders.rs index 10699bac..504ca929 100644 --- a/examples/mail_folders_and_messages/mail_folders.rs +++ b/examples/mail_folders_and_messages/mail_folders.rs @@ -10,7 +10,7 @@ static USER_ID: &str = "USER_ID"; // Get the top 2 inbox messages for a user. pub async fn get_user_inbox_messages() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) .mail_folder("Inbox") @@ -30,7 +30,7 @@ pub async fn get_user_inbox_messages() -> GraphResult<()> { // Get the top 2 inbox messages for a user. pub async fn get_me_inbox_messages() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() .mail_folder("Inbox") @@ -48,7 +48,7 @@ pub async fn get_me_inbox_messages() { } pub async fn create_mail_folder_message() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() .mail_folder(MAIL_FOLDER_ID) @@ -77,7 +77,7 @@ pub async fn create_mail_folder_message() -> GraphResult<()> { } pub async fn create_mail_folder_draft_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() .mail_folder("drafts") @@ -105,7 +105,7 @@ pub async fn create_mail_folder_draft_message() { } pub async fn delete_mail_folder_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() .mail_folder(MAIL_FOLDER_ID) @@ -119,7 +119,7 @@ pub async fn delete_mail_folder_message() { } pub async fn add_mail_folder_message_attachment() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/mail_folders_and_messages/mailbox_settings.rs b/examples/mail_folders_and_messages/mailbox_settings.rs index f2d8e420..ebbc20dc 100644 --- a/examples/mail_folders_and_messages/mailbox_settings.rs +++ b/examples/mail_folders_and_messages/mailbox_settings.rs @@ -6,7 +6,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static USER_ID: &str = "USER_ID"; pub async fn get_mailbox_settings() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -24,7 +24,7 @@ pub async fn get_mailbox_settings() -> GraphResult<()> { } pub async fn get_user_mailbox_settings() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) diff --git a/examples/mail_folders_and_messages/messages.rs b/examples/mail_folders_and_messages/messages.rs index 5f4fe7ef..00ae0acb 100644 --- a/examples/mail_folders_and_messages/messages.rs +++ b/examples/mail_folders_and_messages/messages.rs @@ -10,14 +10,14 @@ static ATTACHMENT_ID: &str = "ATTACHMENT_ID"; static USER_ID: &str = "USER_ID"; pub async fn list_messages() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.me().messages().list_messages().send().await.unwrap(); println!("{response:#?}"); } pub async fn user_list_messages() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) @@ -31,7 +31,7 @@ pub async fn user_list_messages() { } pub async fn delete_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -45,7 +45,7 @@ pub async fn delete_message() { } pub async fn create_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -73,7 +73,7 @@ pub async fn create_message() { } pub async fn update_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -94,7 +94,7 @@ pub async fn update_message() { } pub async fn send_mail() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() diff --git a/examples/oauth/README.md b/examples/oauth/README.md index 4e5bca65..70a02e1d 100644 --- a/examples/oauth/README.md +++ b/examples/oauth/README.md @@ -34,7 +34,7 @@ async fn main() { .unwrap() .build(); - let graph_client = Graph::from(confidential_client); + let graph_client = GraphClient::from(confidential_client); let _response = graph_client .users() diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index 88a1ba95..382ea582 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -33,7 +33,7 @@ async fn auth_code_grant_secret( .unwrap() .build(); - let graph_client = Graph::from(&confidential_client); + let graph_client = GraphClient::from(&confidential_client); let _response = graph_client.users().list_user().send().await; } diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs index 919148b1..37d2d3e6 100644 --- a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs @@ -37,7 +37,7 @@ fn get_graph_client(authorization_code: &str) -> Graph { .with_redirect_uri(REDIRECT_URI) .unwrap() .build(); - Graph::from(&confidential_client) + GraphClient::from(&confidential_client) } /// # Example diff --git a/examples/oauth/client_credentials/client_credentials_secret.rs b/examples/oauth/client_credentials/client_credentials_secret.rs index bcb05e40..abaf80ae 100644 --- a/examples/oauth/client_credentials/client_credentials_secret.rs +++ b/examples/oauth/client_credentials/client_credentials_secret.rs @@ -3,18 +3,18 @@ // has been granted to your app beforehand. If you have not granted admin consent, see // examples/client_credentials_admin_consent.rs for more info. -use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; +use graph_rs_sdk::{oauth::ConfidentialClientApplication, GraphClient}; // Replace client id, client secret, and tenant id with your own values. static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; static TENANT_ID: &str = "<TENANT_ID>"; -pub async fn get_graph_client() -> Graph { +pub async fn get_graph_client() -> GraphClient { let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) .with_client_secret(CLIENT_SECRET) .with_tenant(TENANT_ID) .build(); - Graph::from(&confidential_client_application) + GraphClient::from(&confidential_client_application) } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 5ef82072..5876b432 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -29,7 +29,7 @@ use graph_rs_sdk::oauth::{ DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, TokenCredentialExecutor, }; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; fn main() {} @@ -48,7 +48,7 @@ async fn auth_code_grant( .unwrap() .build(); - let _graph_client = Graph::from(&confidential_client); + let _graph_client = GraphClient::from(&confidential_client); } // Client Credentials Grant @@ -58,5 +58,5 @@ async fn client_credentials() { .with_tenant("TENANT_ID") .build(); - let _graph_client = Graph::from(&confidential_client); + let _graph_client = GraphClient::from(&confidential_client); } diff --git a/examples/oauth/openid/openid.rs b/examples/oauth/openid/openid.rs index 3b3a6c03..2bda3bf8 100644 --- a/examples/oauth/openid/openid.rs +++ b/examples/oauth/openid/openid.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::oauth::{ConfidentialClientApplication, IdToken}; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; // OpenIdCredential will automatically include the openid scope fn get_graph_client( @@ -9,7 +9,7 @@ fn get_graph_client( redirect_uri: &str, scope: Vec<&str>, id_token: IdToken, -) -> Graph { +) -> GraphClient { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_openid(id_token.code.unwrap(), client_secret) .with_tenant(tenant_id) @@ -18,5 +18,5 @@ fn get_graph_client( .with_scope(scope) .build(); - Graph::from(&confidential_client) + GraphClient::from(&confidential_client) } diff --git a/examples/odata_query.rs b/examples/odata_query.rs index 87a3a16e..a3077ce6 100644 --- a/examples/odata_query.rs +++ b/examples/odata_query.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use graph_rs_sdk::{Graph, GraphResult, ODataQuery}; +use graph_rs_sdk::{GraphClient, GraphResult, ODataQuery}; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; @@ -9,7 +9,7 @@ fn main() {} // https://learn.microsoft.com/en-us/graph/query-parameters?tabs=http async fn custom_path() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client .users() @@ -22,7 +22,7 @@ async fn custom_path() -> GraphResult<()> { } async fn top() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client.users().list_user().top("5").send().await?; @@ -30,7 +30,7 @@ async fn top() -> GraphResult<()> { } async fn skip() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client.users().list_user().skip("2").send().await?; @@ -38,7 +38,7 @@ async fn skip() -> GraphResult<()> { } async fn expand() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client .users() @@ -51,7 +51,7 @@ async fn expand() -> GraphResult<()> { } async fn filter() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client .users() @@ -64,7 +64,7 @@ async fn filter() -> GraphResult<()> { } async fn order_by() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client .users() @@ -77,7 +77,7 @@ async fn order_by() -> GraphResult<()> { } async fn format() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client.users().list_user().format("json").send().await?; @@ -85,7 +85,7 @@ async fn format() -> GraphResult<()> { } async fn count() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client.users().list_user().count("true").send().await?; @@ -93,7 +93,7 @@ async fn count() -> GraphResult<()> { } async fn search() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let _ = client.users().list_user().search("pizza").send().await?; diff --git a/examples/onenote/delete_page.rs b/examples/onenote/delete_page.rs index d3bdbd19..bd004ccf 100644 --- a/examples/onenote/delete_page.rs +++ b/examples/onenote/delete_page.rs @@ -8,7 +8,7 @@ static USER_ID: &str = "USER_ID"; static PAGE_ID: &str = "PAGE_ID"; pub async fn delete_page() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) diff --git a/examples/onenote/get_page_content.rs b/examples/onenote/get_page_content.rs index 720a4821..8ea9314e 100644 --- a/examples/onenote/get_page_content.rs +++ b/examples/onenote/get_page_content.rs @@ -19,7 +19,7 @@ static DOWNLOAD_PATH: &str = "DOWNLOAD_PATH"; static FILE_NAME: &str = "FILE_NAME"; pub async fn get_page_html_content() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) @@ -35,7 +35,7 @@ pub async fn get_page_html_content() { } pub async fn download_page_as_html() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) diff --git a/examples/onenote/upload_page_content.rs b/examples/onenote/upload_page_content.rs index b137a97c..90aad587 100644 --- a/examples/onenote/upload_page_content.rs +++ b/examples/onenote/upload_page_content.rs @@ -15,7 +15,7 @@ static FILE_PATH: &str = "./FILE.html"; // here: https://learn.microsoft.com/en-us/graph/api/section-post-pages?view=graph-rest-1.0 pub async fn upload_page_content() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) @@ -31,7 +31,7 @@ pub async fn upload_page_content() { } pub async fn upload_page_content_using_file() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut file = OpenOptions::new().read(true).open(FILE_PATH)?; diff --git a/examples/paging/channel.rs b/examples/paging/channel.rs index a7149af9..550157fb 100644 --- a/examples/paging/channel.rs +++ b/examples/paging/channel.rs @@ -3,7 +3,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; pub async fn channel_next_links() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut receiver = client .users() .list_user() diff --git a/examples/paging/delta.rs b/examples/paging/delta.rs index 72df79ff..f3299420 100644 --- a/examples/paging/delta.rs +++ b/examples/paging/delta.rs @@ -4,7 +4,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; pub async fn channel_delta() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut receiver = client .users() .delta() @@ -27,7 +27,7 @@ pub async fn channel_delta() -> GraphResult<()> { static DELTA_TOKEN: &str = "DELTA_TOKEN"; pub async fn channel_delta_token() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut receiver = client .users() .delta() @@ -48,7 +48,7 @@ pub async fn channel_delta_token() -> GraphResult<()> { } pub async fn stream_delta() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut stream = client .users() .delta() diff --git a/examples/paging/stream.rs b/examples/paging/stream.rs index 5e878492..97ae9fae 100644 --- a/examples/paging/stream.rs +++ b/examples/paging/stream.rs @@ -4,7 +4,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; pub async fn stream_next_links() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut stream = client .users() diff --git a/examples/paging_and_next_links.rs b/examples/paging_and_next_links.rs index 4b2f0e23..9bfc35dd 100644 --- a/examples/paging_and_next_links.rs +++ b/examples/paging_and_next_links.rs @@ -25,7 +25,7 @@ async fn main() { } async fn paging() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let deque = client .users() diff --git a/examples/request_body_helper.rs b/examples/request_body_helper.rs index 2150d590..1ecc0a8e 100644 --- a/examples/request_body_helper.rs +++ b/examples/request_body_helper.rs @@ -20,7 +20,7 @@ body parameter for those api methods that need one. */ use graph_rs_sdk::http::BodyRead; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; use std::fs::File; fn main() {} @@ -35,7 +35,7 @@ fn main() {} fn use_reqwest_blocking_body() { let body = reqwest::blocking::Body::from(String::new()); - let client = Graph::new("token"); + let client = GraphClient::new("token"); client .user("id") .get_mail_tips(body) @@ -47,7 +47,7 @@ fn use_reqwest_blocking_body() { async fn use_reqwest_async_body() { let body = reqwest::Body::from(String::new()); - let client = Graph::new("token"); + let client = GraphClient::new("token"); client.user("id").get_mail_tips(body).send().await.unwrap(); } diff --git a/examples/sites/get_sites.rs b/examples/sites/get_sites.rs index 63f6c664..5576a195 100644 --- a/examples/sites/get_sites.rs +++ b/examples/sites/get_sites.rs @@ -5,7 +5,7 @@ static ACCESS_TOKEN: &str = "<SITE_ID>"; static SITE_ID: &str = "<SITE_ID>"; pub async fn get_site() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.site(SITE_ID).get_site().send().await?; @@ -18,7 +18,7 @@ pub async fn get_site() -> GraphResult<()> { } pub async fn list_sites() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.site(SITE_ID).list_sites().send().await?; diff --git a/examples/sites/lists_items.rs b/examples/sites/lists_items.rs index f8ffcf46..b7283cf8 100644 --- a/examples/sites/lists_items.rs +++ b/examples/sites/lists_items.rs @@ -9,7 +9,7 @@ static LIST_ID: &str = "<LIST_ID>"; static LIST_ITEM_ID: &str = "<LIST_ITEM_ID>"; pub async fn create_list() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) @@ -39,7 +39,7 @@ pub async fn create_list() -> GraphResult<()> { } pub async fn list_all_list_items() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) @@ -58,7 +58,7 @@ pub async fn list_all_list_items() -> GraphResult<()> { } pub async fn create_list_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) @@ -80,7 +80,7 @@ pub async fn create_list_item() -> GraphResult<()> { } pub async fn update_list_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) @@ -103,7 +103,7 @@ pub async fn update_list_item() -> GraphResult<()> { } pub async fn get_list_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) @@ -122,7 +122,7 @@ pub async fn get_list_item() -> GraphResult<()> { } pub async fn delete_list_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .site(SITE_ID) diff --git a/examples/teams/create_team.rs b/examples/teams/create_team.rs index 2988aa38..612c27fe 100644 --- a/examples/teams/create_team.rs +++ b/examples/teams/create_team.rs @@ -17,7 +17,7 @@ pub async fn create_team() { "user@odata.bind": format!("https://graph.microsoft.com/v1.0/users('{OWNER_ID}')") }]}); - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.teams().create_team(&json).send().await.unwrap(); println!("{response:#?}"); diff --git a/examples/teams/get_teams.rs b/examples/teams/get_teams.rs index 07729239..2659376c 100644 --- a/examples/teams/get_teams.rs +++ b/examples/teams/get_teams.rs @@ -7,14 +7,14 @@ static TEAMS_ID: &str = "TEAMS_ID"; // List teams may not be supported on v1.0 endpoint but is supported on beta. pub async fn list_teams() { - let mut client = Graph::new(ACCESS_TOKEN); + let mut client = GraphClient::new(ACCESS_TOKEN); let response = client.beta().teams().list_team().send().await.unwrap(); println!("{response:#?}"); } pub async fn get_teams() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.team(TEAMS_ID).get_team().send().await.unwrap(); println!("{response:#?}"); diff --git a/examples/upload_session/cancel_upload_session.rs b/examples/upload_session/cancel_upload_session.rs index 7cd2b921..a8a40cdd 100644 --- a/examples/upload_session/cancel_upload_session.rs +++ b/examples/upload_session/cancel_upload_session.rs @@ -18,7 +18,7 @@ static PATH_IN_ONE_DRIVE: &str = ":/Documents/file.ext:"; static CONFLICT_BEHAVIOR: &str = "rename"; pub async fn cancel_upload_session(bytes: &[u8]) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some(CONFLICT_BEHAVIOR.to_string()) diff --git a/examples/upload_session/channel_upload_session.rs b/examples/upload_session/channel_upload_session.rs index 9ce7434a..b3028865 100644 --- a/examples/upload_session/channel_upload_session.rs +++ b/examples/upload_session/channel_upload_session.rs @@ -1,6 +1,6 @@ use graph_error::GraphResult; use graph_http::traits::ResponseExt; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; @@ -13,7 +13,7 @@ static PATH_IN_ONE_DRIVE: &str = ":/Documents/file.ext:"; static CONFLICT_BEHAVIOR: &str = "rename"; pub async fn channel(file: tokio::fs::File) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some("fail".to_string()) diff --git a/examples/upload_session/stream_upload_session.rs b/examples/upload_session/stream_upload_session.rs index 9a0aad8d..0431d90b 100644 --- a/examples/upload_session/stream_upload_session.rs +++ b/examples/upload_session/stream_upload_session.rs @@ -15,7 +15,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static PATH_IN_ONE_DRIVE: &str = ":/Documents/file.ext:"; pub async fn stream(bytes: BytesMut) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some("fail".to_string()) diff --git a/examples/upload_session/upload_bytes_iterator.rs b/examples/upload_session/upload_bytes_iterator.rs index cc55cbb3..3538a01d 100644 --- a/examples/upload_session/upload_bytes_iterator.rs +++ b/examples/upload_session/upload_bytes_iterator.rs @@ -26,7 +26,7 @@ static CONFLICT_BEHAVIOR: &str = "rename"; /// Use [`while let Some(result) = upload_session.next()`] when using Iterator impl. /// DO NOT use [`for result in upload_session.next()`] when using Iterator impl. pub async fn upload_bytes(bytes: Bytes) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some(CONFLICT_BEHAVIOR.to_string()) @@ -52,7 +52,7 @@ pub async fn upload_bytes(bytes: Bytes) -> GraphResult<()> { } pub async fn upload_vec_u8(bytes: &[u8]) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some(CONFLICT_BEHAVIOR.to_string()) diff --git a/examples/upload_session/upload_file_iterator.rs b/examples/upload_session/upload_file_iterator.rs index 615226b9..20c0ebff 100644 --- a/examples/upload_session/upload_file_iterator.rs +++ b/examples/upload_session/upload_file_iterator.rs @@ -30,7 +30,7 @@ static CONFLICT_BEHAVIOR: &str = "rename"; /// Use [`while let Some(result) = upload_session.next()`] when using Iterator impl. /// DO NOT use [`for result in upload_session.next()`] when using Iterator impl. pub async fn upload_file(file: std::fs::File) -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some(CONFLICT_BEHAVIOR.to_string()) @@ -56,7 +56,7 @@ pub async fn upload_file(file: std::fs::File) -> GraphResult<()> { } pub async fn upload_file_async_read() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let upload = serde_json::json!({ "@microsoft.graph.conflictBehavior": Some(CONFLICT_BEHAVIOR.to_string()) diff --git a/examples/users/todos/tasks.rs b/examples/users/todos/tasks.rs index 7aa678e1..05f250ef 100644 --- a/examples/users/todos/tasks.rs +++ b/examples/users/todos/tasks.rs @@ -1,6 +1,6 @@ use futures::StreamExt; use graph_rs_sdk::error::GraphResult; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; use std::collections::VecDeque; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -23,7 +23,7 @@ pub struct TodoListTaskCollection { } async fn list_tasks(user_id: &str, list_id: &str) -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let mut stream = client .user(user_id) @@ -46,7 +46,7 @@ async fn list_tasks(user_id: &str, list_id: &str) -> GraphResult<()> { } async fn create_task(user_id: &str, list_id: &str) -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let task = &serde_json::json!( { @@ -79,7 +79,7 @@ async fn create_task(user_id: &str, list_id: &str) -> GraphResult<()> { } async fn create_task_using_me(list_id: &str) -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let task = &serde_json::json!( { diff --git a/examples/users/user.rs b/examples/users/user.rs index e2f553b4..1f788157 100644 --- a/examples/users/user.rs +++ b/examples/users/user.rs @@ -10,12 +10,12 @@ // Delegate (Personal microsoft accounts) are not supported in the Graph API. use graph_error::GraphResult; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; static USER_ID: &str = "USER_ID"; async fn list_users() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client.users().list_user().send().await?; @@ -28,7 +28,7 @@ async fn list_users() -> GraphResult<()> { } async fn get_user() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client.user(USER_ID).get_user().send().await?; println!("{response:#?}"); @@ -40,7 +40,7 @@ async fn get_user() -> GraphResult<()> { } async fn create_user() { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); // Create a password profile. Change the password below // to one that meets the Microsoft password requirements. @@ -70,7 +70,7 @@ async fn create_user() { // need to be updated. Properties that are left alone // will stay the same. async fn update_user() { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let user = serde_json::json!({ "business_phones": ["888-888-8888"] @@ -87,7 +87,7 @@ async fn update_user() { } async fn delete_user() { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client.user(USER_ID).delete_user().send().await.unwrap(); diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index 9faf2b41..386b619f 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -3,21 +3,12 @@ use std::sync::mpsc::RecvError; #[derive(Debug, thiserror::Error)] pub enum WebViewExecutionError { + #[error("WindowClosed: {0:#?}")] + WindowClosed(String), // Issues with the redirect uri such as specifying localhost // but not providing a port in the WebViewOptions. #[error("InvalidRedirectUri: {0:#?}")] InvalidRedirectUri(String), - /// The user closed the webview window without logging in. - #[error("WindowClosedRequested")] - WindowClosedRequested, - /// The user navigated to a url that was not the login url - /// or a redirect url specified. Requires that WebViewOptions - /// has the enforcement of invalid navigation enabled. - #[error("WindowClosedOnInvalidNavigation")] - WindowClosedOnInvalidNavigation, - /// The webview exited because of a timeout defined in the WebViewOptions. - #[error("WindowClosedOnTimeoutReached")] - WindowClosedOnTimeoutReached, /// The host or domain provided or set for login is invalid. /// This could be an internal error and most likely will never happen. #[error("InvalidStartUri: {reason:#?}")] diff --git a/src/client/graph.rs b/src/client/graph.rs index 529ba993..c5507044 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -79,24 +79,27 @@ lazy_static! { Url::parse(GRAPH_URL_BETA).expect("Unable to set beta endpoint"); } +// For backwards compatibility. +pub type Graph = GraphClient; + #[derive(Debug, Clone)] -pub struct Graph { +pub struct GraphClient { client: Client, endpoint: Url, allowed_host_validator: AllowedHostValidator, } -impl Graph { - pub fn new<AT: ToString>(access_token: AT) -> Graph { - Graph { +impl GraphClient { + pub fn new<AT: ToString>(access_token: AT) -> GraphClient { + GraphClient { client: Client::new(BearerTokenCredential::from(access_token.to_string())), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), } } - pub fn from_client_app<CA: ClientApplication + 'static>(client_app: CA) -> Graph { - Graph { + pub fn from_client_app<CA: ClientApplication + 'static>(client_app: CA) -> GraphClient { + GraphClient { client: Client::new(client_app), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), @@ -526,27 +529,27 @@ impl Graph { } } -impl From<&str> for Graph { +impl From<&str> for GraphClient { fn from(token: &str) -> Self { - Graph::from_client_app(BearerTokenCredential::from(token.to_string())) + GraphClient::from_client_app(BearerTokenCredential::from(token.to_string())) } } -impl From<String> for Graph { +impl From<String> for GraphClient { fn from(token: String) -> Self { - Graph::from_client_app(BearerTokenCredential::from(token)) + GraphClient::from_client_app(BearerTokenCredential::from(token)) } } -impl From<&Token> for Graph { +impl From<&Token> for GraphClient { fn from(token: &Token) -> Self { - Graph::from_client_app(BearerTokenCredential::from(token.access_token.clone())) + GraphClient::from_client_app(BearerTokenCredential::from(token.access_token.clone())) } } -impl From<GraphClientConfiguration> for Graph { +impl From<GraphClientConfiguration> for GraphClient { fn from(graph_client_builder: GraphClientConfiguration) -> Self { - Graph { + GraphClient { client: graph_client_builder.build(), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), @@ -554,51 +557,51 @@ impl From<GraphClientConfiguration> for Graph { } } -impl From<&ConfidentialClientApplication<AuthorizationCodeCredential>> for Graph { +impl From<&ConfidentialClientApplication<AuthorizationCodeCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<AuthorizationCodeCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<AuthorizationCodeAssertionCredential>> for Graph { +impl From<&ConfidentialClientApplication<AuthorizationCodeAssertionCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<AuthorizationCodeAssertionCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> for Graph { +impl From<&ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<AuthorizationCodeCertificateCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<ClientSecretCredential>> for Graph { +impl From<&ConfidentialClientApplication<ClientSecretCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<ClientSecretCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<ClientCertificateCredential>> for Graph { +impl From<&ConfidentialClientApplication<ClientCertificateCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<ClientCertificateCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<ClientAssertionCredential>> for Graph { +impl From<&ConfidentialClientApplication<ClientAssertionCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<ClientAssertionCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&ConfidentialClientApplication<OpenIdCredential>> for Graph { +impl From<&ConfidentialClientApplication<OpenIdCredential>> for GraphClient { fn from(value: &ConfidentialClientApplication<OpenIdCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } -impl From<&PublicClientApplication<DeviceCodeCredential>> for Graph { +impl From<&PublicClientApplication<DeviceCodeCredential>> for GraphClient { fn from(value: &PublicClientApplication<DeviceCodeCredential>) -> Self { - Graph::from_client_app(value.clone()) + GraphClient::from_client_app(value.clone()) } } @@ -609,56 +612,56 @@ mod test { #[test] #[should_panic] fn try_invalid_host() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.custom_endpoint("https://example.org"); } #[test] #[should_panic] fn try_invalid_scheme() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.custom_endpoint("http://example.org"); } #[test] #[should_panic] fn try_invalid_query() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.custom_endpoint("https://example.org?user=name"); } #[test] #[should_panic] fn try_invalid_path() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.custom_endpoint("https://example.org/v1"); } #[test] #[should_panic] fn try_invalid_host2() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.use_endpoint("https://example.org"); } #[test] #[should_panic] fn try_invalid_scheme2() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.use_endpoint("http://example.org"); } #[test] #[should_panic] fn try_invalid_query2() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.use_endpoint("https://example.org?user=name"); } #[test] #[should_panic] fn try_invalid_path2() { - let mut client = Graph::new("token"); + let mut client = GraphClient::new("token"); client.use_endpoint("https://example.org/v1"); } diff --git a/src/lib.rs b/src/lib.rs index ed6d0d33..4b4d0c21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,7 +229,7 @@ pub mod users; pub static GRAPH_URL: &str = "https://graph.microsoft.com/v1.0"; pub static GRAPH_URL_BETA: &str = "https://graph.microsoft.com/beta"; -pub use crate::client::Graph; +pub use crate::client::{Graph, GraphClient}; pub use graph_error::{GraphFailure, GraphResult}; pub use graph_http::api_impl::{GraphClientConfiguration, ODataQuery}; diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 270036be..17dfc562 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -225,7 +225,7 @@ impl OAuthTestClient { pub fn request_access_token(&self) -> Option<(String, Token)> { if Environment::is_local() || Environment::is_travis() { - let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, }; @@ -233,9 +233,11 @@ impl OAuthTestClient { } else if Environment::is_appveyor() { self.get_access_token(OAuthTestCredentials::new_env()) } else if Environment::is_github() { - let map: OAuthTestClientMap = - serde_json::from_str(&env::var("TEST_CREDENTIALS").unwrap()).unwrap(); - self.get_access_token(map.get(self).unwrap()) + let map = OAuthTestClient::get_app_registration()?; + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_access_token(test_client_map.get(self).unwrap()) } else { None } @@ -243,15 +245,17 @@ impl OAuthTestClient { pub fn request_access_token_credential(&self) -> Option<(String, impl ClientApplication)> { if Environment::is_local() || Environment::is_travis() { - let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, }; self.get_credential(test_client_map.get(self).unwrap()) } else if Environment::is_github() { - let map: OAuthTestClientMap = - serde_json::from_str(&env::var("TEST_CREDENTIALS").unwrap()).unwrap(); - self.get_credential(map.get(self).unwrap()) + let map = OAuthTestClient::get_app_registration()?; + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_credential(test_client_map.get(self).unwrap()) } else { None } @@ -259,7 +263,7 @@ impl OAuthTestClient { pub async fn request_access_token_async(&self) -> Option<(String, Token)> { if Environment::is_local() || Environment::is_travis() { - let map = AppRegistrationMap::from_file("./app_registrations.json").unwrap(); + let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, }; @@ -269,9 +273,12 @@ impl OAuthTestClient { self.get_access_token_async(OAuthTestCredentials::new_env()) .await } else if Environment::is_github() { - let map: OAuthTestClientMap = - serde_json::from_str(&env::var("TEST_CREDENTIALS").unwrap()).unwrap(); - self.get_access_token_async(map.get(self).unwrap()).await + let map = OAuthTestClient::get_app_registration()?; + let test_client_map = OAuthTestClientMap { + clients: map.get_default_client_credentials().clients, + }; + self.get_access_token_async(test_client_map.get(self).unwrap()) + .await } else { None } From d9dcf615998d5f6e315396ea7b93ce2c90d69094 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Mon, 13 Nov 2023 00:43:28 -0500 Subject: [PATCH 062/118] Fix imports for webview and update returned errors --- .gitignore | 1 + Cargo.toml | 5 +- examples/interactive_authentication/README.md | 10 +- .../{webview.rs => auth_code.rs} | 37 +-- examples/interactive_authentication/main.rs | 5 +- examples/interactive_authentication/openid.rs | 32 ++ .../webview_errors.rs | 52 +-- .../server_examples/auth_code_grant_pkce.rs | 11 +- .../server_examples/auth_code_grant_secret.rs | 13 +- examples/oauth/main.rs | 2 + .../oauth/openid/server_examples/openid.rs | 18 +- graph-error/src/lib.rs | 4 +- graph-error/src/webview_error.rs | 104 +++--- .../src/blocking/blocking_request_handler.rs | 3 +- graph-http/src/client.rs | 46 --- graph-http/src/request_handler.rs | 46 ++- .../identity/authorization_query_response.rs | 42 ++- ...ion_serializer.rs => authorization_url.rs} | 16 +- .../src/identity/credentials/app_config.rs | 21 +- .../auth_code_authorization_url.rs | 198 ++++------- .../authorization_code_credential.rs | 1 + .../credentials/device_code_credential.rs | 314 +++++++----------- .../credentials/open_id_authorization_url.rs | 215 ++++++++++-- .../credentials/open_id_credential.rs | 40 +++ .../credentials/token_credential_executor.rs | 26 +- graph-oauth/src/identity/mod.rs | 4 +- .../src/web/interactive_authenticator.rs | 44 ++- graph-oauth/src/web/interactive_web_view.rs | 29 +- graph-oauth/src/web/web_view_options.rs | 1 + test-tools/src/oauth_request.rs | 25 +- 30 files changed, 712 insertions(+), 653 deletions(-) rename examples/interactive_authentication/{webview.rs => auth_code.rs} (66%) create mode 100644 examples/interactive_authentication/openid.rs rename graph-oauth/src/identity/{authorization_serializer.rs => authorization_url.rs} (52%) diff --git a/.gitignore b/.gitignore index 69049949..afb12e63 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,6 @@ env.toml env.json env.yaml app_registrations.json +app_registrations2.json graph-codegen/src/parsed_metadata/** diff --git a/Cargo.toml b/Cargo.toml index bcb7dc42..980ba531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,10 +67,7 @@ warp = { version = "0.3.5" } webbrowser = "0.8.7" anyhow = "1.0.69" log = "0.4" -pretty_env_logger = "0.4" -from_as = "0.2.0" -tracing = "0.1.37" -tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +pretty_env_logger = "0.5.0" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/interactive_authentication/README.md b/examples/interactive_authentication/README.md index 87b544a9..f1c28580 100644 --- a/examples/interactive_authentication/README.md +++ b/examples/interactive_authentication/README.md @@ -66,8 +66,8 @@ async fn authenticate() { ### WebView Options -You can customize several aspects of the webview including security mechanisms -or setting an OS theme. +You can customize several aspects of the webview and how the webview is used to perform interactive auth +using `WebViewOptions`. ```rust use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; @@ -81,12 +81,6 @@ fn get_webview_options() -> WebViewOptions { // OS specific theme. Does not work on all operating systems. // See wry crate for more info. .with_theme(Theme::Dark) - // Close the webview window whenever there is a navigation by the webview or user - // to a url that is not one of the redirect urls or the login url. - // For instance, if this is considered a security issue and the user should - // not be able to navigate to another url. - // Either way, the url bar does not show regardless. - .with_close_window_on_invalid_navigation(true) // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the // user to log in and the user has not logged in after 20 minutes you may diff --git a/examples/interactive_authentication/webview.rs b/examples/interactive_authentication/auth_code.rs similarity index 66% rename from examples/interactive_authentication/webview.rs rename to examples/interactive_authentication/auth_code.rs index bffa6873..a34abb87 100644 --- a/examples/interactive_authentication/webview.rs +++ b/examples/interactive_authentication/auth_code.rs @@ -1,9 +1,4 @@ -use graph_rs_sdk::oauth::{ - web::Theme, web::WebViewOptions, AuthorizationCodeCredential, TokenCredentialExecutor, -}; -use graph_rs_sdk::GraphClient; -use std::ops::Add; -use std::time::{Duration, Instant}; +use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; static CLIENT_ID: &str = "CLIENT_ID"; static CLIENT_SECRET: &str = "CLIENT_SECRET"; @@ -34,20 +29,18 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // by requesting the offline_access scope, then the confidential client will take care of refreshing // the token. async fn authenticate() { - // Create a tracing subscriber to log debug/trace events coming from - // authorization http calls and the Graph client. - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - .with_max_level(tracing::Level::TRACE) - .init(); - - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .with_interactive_authentication(None) - .unwrap(); + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + + let (authorization_query_response, mut credential_builder) = + AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT_ID) + .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(REDIRECT_URI) + .with_interactive_authentication(None) + .unwrap(); + + debug!("{authorization_query_response:#?}"); let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); @@ -55,7 +48,7 @@ async fn authenticate() { let response = client.user(USER_ID).get_user().send().await.unwrap(); - println!("{response:#?}"); + debug!("{response:#?}"); let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); + debug!("{body:#?}"); } diff --git a/examples/interactive_authentication/main.rs b/examples/interactive_authentication/main.rs index ebf0ccda..7139fc4d 100644 --- a/examples/interactive_authentication/main.rs +++ b/examples/interactive_authentication/main.rs @@ -1,6 +1,9 @@ #![allow(dead_code, unused, unused_imports)] -mod webview; +#[macro_use] +extern crate log; +mod auth_code; +mod openid; mod webview_errors; mod webview_options; diff --git a/examples/interactive_authentication/openid.rs b/examples/interactive_authentication/openid.rs new file mode 100644 index 00000000..0e0240d0 --- /dev/null +++ b/examples/interactive_authentication/openid.rs @@ -0,0 +1,32 @@ +use graph_rs_sdk::{ + oauth::{OpenIdCredential, ResponseMode, ResponseType}, + GraphClient, +}; + +fn openid_authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + + let (authorization_query_response, mut credential_builder) = + OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. + .with_response_mode(ResponseMode::Fragment) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_redirect_uri(redirect_uri) + .unwrap() + .with_interactive_authentication(None) + .unwrap(); + + debug!("{authorization_query_response:#?}"); + + let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + + let client = GraphClient::from(&confidential_client); + + let response = client.users().list_user().into_blocking().send().unwrap(); + + debug!("{response:#?}"); + let body: serde_json::Value = response.json().unwrap(); + debug!("{body:#?}"); +} diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs index 1a2a945c..9b6539d6 100644 --- a/examples/interactive_authentication/webview_errors.rs +++ b/examples/interactive_authentication/webview_errors.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::{error::WebViewExecutionError, oauth::AuthorizationCodeCredential}; +use graph_rs_sdk::{error::WebViewError, oauth::AuthorizationCodeCredential}; async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { let mut credential_builder_result = @@ -8,31 +8,37 @@ async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, re .with_redirect_uri(redirect_uri) .with_interactive_authentication(None); - if let Ok(credential_builder) = credential_builder_result { + if let Ok((authorization_query_response, credential_builder)) = credential_builder_result { // ... } else if let Err(err) = credential_builder_result { match err { - // WebView Window Closed Before Sign In and Redirect. - WebViewExecutionError::WindowClosed(reason) => {} - // Issues with the redirect uri such as specifying localhost - // but not providing a port in the WebViewOptions. - WebViewExecutionError::InvalidRedirectUri(uri) => {} - // The host or domain provided or set for login is invalid. - // This could be an internal error and most likely will never happen. - WebViewExecutionError::InvalidStartUri { reason } => {} - // The webview was successfully redirected but the url did not - // contain a query or fragment. The query or fragment of the url - // is where the auth code would be returned to the app. - WebViewExecutionError::RedirectUriMissingQueryOrFragment(_) => {} - // Serde serialization error when attempting to serialize - // the query or fragment of the url to a AuthorizationQueryResponse - WebViewExecutionError::SerdeError(_) => {} - // Error from AuthorizationCodeCredential Authorization Url Builder: AuthCodeAuthorizationUrlParameters - // This most likely came from an invalid parameter or missing parameter - // passed to the client used for building the url. See graph_rs_sdk::oauth::AuthCodeAuthorizationUrlParameters - WebViewExecutionError::AuthorizationError(authorization_failure) => {} - WebViewExecutionError::RecvError(_) => {} - WebViewExecutionError::AuthExecutionError(_) => {} + // Webview Window closed for one of the following reasons: + // 1. The user closed the webview window without logging in. + // 2. The webview exited because of a timeout defined in the WebViewOptions. + WebViewError::WindowClosed(reason) => {} + + // One of the following errors has occurred: + // + // 1. Issues with the redirect uri such as specifying localhost + // but not providing a port in the WebViewOptions. + // + // 2. The webview was successfully redirected but the url did not + // contain a query or fragment. The query or fragment of the url + // is where the auth code would be returned to the app. + // + // 3. The host or domain provided or set for login is invalid. + // This could be an internal error and most likely will never happen. + WebViewError::InvalidUri(reason) => {} + + // The query or fragment of the redirect uri is an error returned + // from Microsoft. + WebViewError::AuthorizationQuery { + error, + error_description, + error_uri, + } => {} + + WebViewError::AuthExecutionError(_) => {} } } } diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs index e2bcafaa..ec326d42 100644 --- a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs @@ -52,7 +52,7 @@ async fn handle_redirect( match code_option { Some(access_code) => { // Print out the code for debugging purposes. - println!("{:#?}", access_code.code); + debug!("{:#?}", access_code.code); let authorization_code = access_code.code; let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) @@ -65,17 +65,17 @@ async fn handle_redirect( // Returns reqwest::Response let response = confidential_client.execute_async().await.unwrap(); - println!("{response:#?}"); + debug!("{response:#?}"); if response.status().is_success() { let access_token: Token = response.json().await.unwrap(); // If all went well here we can print out the OAuth config with the Access Token. - println!("AccessToken: {:#?}", access_token.access_token); + debug!("AccessToken: {:#?}", access_token.access_token); } else { // See if Microsoft Graph returned an error in the Response body let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); + debug!("{result:#?}"); return Ok(Box::new("Error Logging In! You can close your browser.")); } @@ -98,6 +98,9 @@ async fn handle_redirect( /// } /// ``` pub async fn start_server_main() { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + let query = warp::query::<AccessCode>() .map(Some) .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs index 37d2d3e6..030c1f73 100644 --- a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs @@ -50,6 +50,9 @@ fn get_graph_client(authorization_code: &str) -> Graph { /// } /// ``` pub async fn start_server_main() { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + let query = warp::query::<AccessCode>() .map(Some) .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); @@ -84,7 +87,7 @@ async fn handle_redirect( match code_option { Some(access_code) => { // Print out the code for debugging purposes. - println!("{access_code:#?}"); + debug!("{access_code:#?}"); let authorization_code = access_code.code; let client = get_graph_client(authorization_code.as_str()); @@ -92,15 +95,15 @@ async fn handle_redirect( match result { Ok(response) => { - println!("{response:#?}"); + debug!("{response:#?}"); let status = response.status(); let body: serde_json::Value = response.json().await.unwrap(); - println!("Status: {status:#?}"); - println!("Body: {body:#?}"); + debug!("Status: {status:#?}"); + debug!("Body: {body:#?}"); } Err(err) => { - println!("{err:#?}"); + debug!("{err:#?}"); } } diff --git a/examples/oauth/main.rs b/examples/oauth/main.rs index 5876b432..075bb39c 100644 --- a/examples/oauth/main.rs +++ b/examples/oauth/main.rs @@ -14,6 +14,8 @@ #[macro_use] extern crate serde; +#[macro_use] +extern crate log; mod auth_code_grant; mod client_credentials; diff --git a/examples/oauth/openid/server_examples/openid.rs b/examples/oauth/openid/server_examples/openid.rs index a3d971b3..895dfe14 100644 --- a/examples/oauth/openid/server_examples/openid.rs +++ b/examples/oauth/openid/server_examples/openid.rs @@ -2,7 +2,6 @@ use graph_rs_sdk::oauth::{ ConfidentialClientApplication, IdToken, OpenIdCredential, Prompt, ResponseMode, ResponseType, Token, TokenCredentialExecutor, }; -use tracing_subscriber::fmt::format::FmtSpan; use url::Url; /// # Example @@ -86,21 +85,8 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, /// } /// ``` pub async fn start_server_main() { - let filter = - std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=debug,warp=debug".to_owned()); - - // Configure the default `tracing` subscriber. - // The `fmt` subscriber from the `tracing-subscriber` crate logs `tracing` - // events to stdout. Other subscribers are available for integrating with - // distributed tracing systems such as OpenTelemetry. - tracing_subscriber::fmt() - .with_span_events(FmtSpan::FULL) - // Use the filter we built above to determine which traces to record. - .with_env_filter(filter) - // Record an event when each span closes. This can be used to time our - // routes' durations! - .with_span_events(FmtSpan::CLOSE) - .init(); + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); let routes = warp::post() .and(warp::path("redirect")) diff --git a/graph-error/src/lib.rs b/graph-error/src/lib.rs index a936a4c1..d268dd78 100644 --- a/graph-error/src/lib.rs +++ b/graph-error/src/lib.rs @@ -21,5 +21,5 @@ pub type GraphResult<T> = Result<T, GraphFailure>; pub type IdentityResult<T> = Result<T, AuthorizationFailure>; pub type AuthExecutionResult<T> = Result<T, AuthExecutionError>; pub type AuthTaskExecutionResult<T, R> = Result<T, AuthTaskExecutionError<R>>; -pub type WebViewResult<T> = Result<T, WebViewExecutionError>; -pub type DeviceCodeWebViewResult<T> = Result<T, WebViewDeviceCodeExecutionError>; +pub type WebViewResult<T> = Result<T, WebViewError>; +pub type DeviceCodeWebViewResult<T> = Result<T, WebViewDeviceCodeError>; diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index 386b619f..726c115f 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -1,78 +1,52 @@ -use crate::{AuthExecutionError, AuthorizationFailure, ErrorMessage}; -use std::sync::mpsc::RecvError; +use crate::{AuthExecutionError, ErrorMessage}; #[derive(Debug, thiserror::Error)] -pub enum WebViewExecutionError { +pub enum WebViewError { + /// Webview Window closed for one of the following reasons: + /// 1. The user closed the webview window without logging in. + /// 2. The webview exited because of a timeout defined in the WebViewOptions. #[error("WindowClosed: {0:#?}")] WindowClosed(String), - // Issues with the redirect uri such as specifying localhost - // but not providing a port in the WebViewOptions. - #[error("InvalidRedirectUri: {0:#?}")] - InvalidRedirectUri(String), - /// The host or domain provided or set for login is invalid. - /// This could be an internal error and most likely will never happen. - #[error("InvalidStartUri: {reason:#?}")] - InvalidStartUri { reason: String }, - /// The webview was successfully redirected but the url did not - /// contain a query or fragment. The query or fragment of the url - /// is where the auth code would be returned to the app. - #[error("No query or fragment returned on redirect uri: {0:#?}")] - RedirectUriMissingQueryOrFragment(String), - /// Serde serialization error when attempting to serialize - /// the query or fragment of the url to a AuthorizationQueryResponse - #[error("{0:#?}")] - SerdeError(#[from] serde::de::value::Error), - #[error("{0:#?}")] - RecvError(#[from] RecvError), - /// Error from building out the parameters necessary for authorization - /// this most likely came from an invalid parameter or missing parameter - /// passed to the client used for building the url. - #[error("{0:#?}")] - AuthorizationError(#[from] AuthorizationFailure), - /// Error that happens calling the http request. - #[error("{0:#?}")] - AuthExecutionError(#[from] AuthExecutionError), + /// One of the following errors has occurred: + /// + /// 1. Issues with the redirect uri such as specifying localhost + /// but not providing a port in the WebViewOptions. + /// + /// 2. The webview was successfully redirected but the url did not + /// contain a query or fragment. The query or fragment of the url + /// is where the auth code would be returned to the app. + /// + /// 3. The host or domain provided or set for login is invalid. + /// This could be an internal error and most likely will never happen. + #[error("{0:#?}")] + InvalidUri(String), + + /// The query or fragment of the redirect uri is an error returned + /// from Microsoft. + #[error("{error:#?}, {error_description:#?}, {error_uri:#?}")] + AuthorizationQuery { + error: String, + error_description: String, + error_uri: Option<String>, + }, + /// Error that happens when building or calling the http request. + #[error("{0:#?}")] + AuthExecutionError(#[from] Box<AuthExecutionError>), } +impl WebViewError {} + #[derive(Debug, thiserror::Error)] -pub enum WebViewDeviceCodeExecutionError { - // Issues with the redirect uri such as specifying localhost - // but not providing a port in the WebViewOptions. - #[error("InvalidRedirectUri: {0:#?}")] - InvalidRedirectUri(String), - /// The user closed the webview window without logging in. - #[error("WindowClosedRequested")] - WindowClosedRequested, - /// The user navigated to a url that was not the login url - /// or a redirect url specified. Requires that WebViewOptions - /// has the enforcement of invalid navigation enabled. - #[error("WindowClosedOnInvalidNavigation")] - WindowClosedOnInvalidNavigation, - /// The webview exited because of a timeout defined in the WebViewOptions. - #[error("WindowClosedOnTimeoutReached")] - WindowClosedOnTimeoutReached, - /// The host or domain provided or set for login is invalid. - /// This could be an internal error and most likely will never happen. - #[error("InvalidStartUri: {reason:#?}")] - InvalidStartUri { reason: String }, - /// The webview was successfully redirected but the url did not - /// contain a query or fragment. The query or fragment of the url - /// is where the auth code would be returned to the app. - #[error("No query or fragment returned on redirect uri: {0:#?}")] - RedirectUriMissingQueryOrFragment(String), - /// Serde serialization error when attempting to serialize - /// the query or fragment of the url to a AuthorizationQueryResponse - #[error("{0:#?}")] - SerdeError(#[from] serde::de::value::Error), - /// Error from building out the parameters necessary for authorization - /// this most likely came from an invalid parameter or missing parameter - /// passed to the client used for building the url. - #[error("{0:#?}")] - AuthorizationError(#[from] AuthorizationFailure), +pub enum WebViewDeviceCodeError { + /// Webview Window closed for one of the following reasons: + /// 1. The user closed the webview window without logging in. + /// 2. The webview exited because of a timeout defined in the WebViewOptions. + #[error("WindowClosed: {0:#?}")] + WindowClosed(String), /// Error that happens calling the http request. #[error("{0:#?}")] AuthExecutionError(#[from] AuthExecutionError), #[error("{0:#?}")] - DeviceCodeAuthFailed(http::Response<Result<serde_json::Value, ErrorMessage>>), + DeviceCodePollingError(http::Response<Result<serde_json::Value, ErrorMessage>>), } diff --git a/graph-http/src/blocking/blocking_request_handler.rs b/graph-http/src/blocking/blocking_request_handler.rs index df5acb0d..a4ea59c1 100644 --- a/graph-http/src/blocking/blocking_request_handler.rs +++ b/graph-http/src/blocking/blocking_request_handler.rs @@ -26,11 +26,12 @@ impl BlockingRequestHandler { let mut error = None; if let Some(err) = err { + let message = err.to_string(); error = Some(GraphFailure::PreFlightError { url: Some(request_components.url.clone()), headers: Some(request_components.headers.clone()), error: Some(Box::new(err)), - message: String::from("N/A"), + message, }); } diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 0bdb7495..a27fb2f7 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -84,28 +84,6 @@ impl GraphClientConfiguration { self } - /* - pub fn confidential_client_application< - Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, - >( - mut self, - confidential_client: ConfidentialClientApplication<Credential>, - ) -> Self { - self.config.client_application = Some(Box::new(confidential_client)); - self - } - - pub fn public_client_application< - Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static, - >( - mut self, - public_client: PublicClientApplication<Credential>, - ) -> Self { - self.config.client_application = Some(Box::new(public_client)); - self - } - */ - pub fn default_headers(mut self, headers: HeaderMap) -> GraphClientConfiguration { for (key, value) in headers.iter() { self.config.headers.insert(key, value.clone()); @@ -296,30 +274,6 @@ impl Debug for Client { } } -/* -impl From<BearerTokenCredential> for Client { - fn from(value: BearerTokenCredential) -> Self { - Client::new(value) - } -} - -impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static> - From<PublicClientApplication<Credential>> for Client -{ - fn from(value: PublicClientApplication<Credential>) -> Self { - Client::new(value) - } -} - -impl<Credential: Clone + Send + TokenCredentialExecutor + TokenCacheStore + 'static> - From<ConfidentialClientApplication<Credential>> for Client -{ - fn from(value: ConfidentialClientApplication<Credential>) -> Self { - Client::new(value) - } -} - */ - #[cfg(test)] mod test { use super::*; diff --git a/graph-http/src/request_handler.rs b/graph-http/src/request_handler.rs index 15937b56..6a50f216 100644 --- a/graph-http/src/request_handler.rs +++ b/graph-http/src/request_handler.rs @@ -36,11 +36,12 @@ impl RequestHandler { let mut error = None; if let Some(err) = err { + let message = err.to_string(); error = Some(GraphFailure::PreFlightError { url: Some(request_components.url.clone()), headers: Some(request_components.headers.clone()), error: Some(Box::new(err)), - message: String::from("N/A"), + message, }); } @@ -293,16 +294,37 @@ impl Paging { /// /// # Example /// ```rust,ignore - /// let mut stream = client - /// .users() - /// .delta() - /// .paging() - /// .stream::<serde_json::Value>() - /// .unwrap(); + /// #[derive(Debug, Serialize, Deserialize)] + /// pub struct User { + /// pub(crate) id: Option<String>, + /// #[serde(rename = "userPrincipalName")] + /// user_principal_name: Option<String>, + /// } + /// + /// #[derive(Debug, Serialize, Deserialize)] + /// pub struct Users { + /// pub value: Vec<User>, + /// } + /// + /// #[tokio::main] + /// async fn main() -> GraphResult<()> { + /// let client = GraphClient::new("ACCESS_TOKEN"); + /// + /// let deque = client + /// .users() + /// .list_user() + /// .select(&["id", "userPrincipalName"]) + /// .paging() + /// .json::<Users>() + /// .await?; + /// + /// for response in deque.iter() { + /// let users = response.into_body()?; + /// println!("{users:#?}"); + /// } + /// Ok(()) + /// } /// - /// while let Some(result) = stream.next().await { - /// println!("{result:#?}"); - /// } /// ``` pub async fn json<T: DeserializeOwned>(mut self) -> GraphResult<VecDeque<PagingResponse<T>>> { if let Some(err) = self.0.error { @@ -434,7 +456,7 @@ impl Paging { /// .list_user() /// .top("5") /// .paging() - /// .channel::<serde_json::Value>() + /// .channel_timeout::<serde_json::Value>(Duration::from_secs(60)) /// .await?; /// /// while let Some(result) = receiver.recv().await { @@ -477,7 +499,7 @@ impl Paging { /// .list_user() /// .top("5") /// .paging() - /// .channel::<serde_json::Value>() + /// .channel_buffer_timeout::<serde_json::Value>(100, Duration::from_secs(60)) /// .await?; /// /// while let Some(result) = receiver.recv().await { diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index 6013bf7b..d97bcc04 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,10 +1,14 @@ use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use serde_json::Value; use url::Url; +/// The specification defines theres errors here: /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-31#section-4.2.2.1 +/// +/// Microsoft has additional errors listed here: +/// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#error-codes-for-authorization-endpoint-errors #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum AuthorizationQueryError { /// The request is missing a required parameter, includes an @@ -47,6 +51,37 @@ pub enum AuthorizationQueryError { /// to the client via a HTTP redirect.) #[serde(alias = "temporarily_unavailable", alias = "TemporarilyUnavailable")] TemporarilyUnavailable, + + /// The target resource is invalid because it doesn't exist, Microsoft Entra ID can't find it, + /// or it's not correctly configured. + /// + /// The client requested silent authentication (prompt=none), but a single user couldn't be + /// found. This error may mean there are multiple users active in the session, or no users. + /// This error takes into account the tenant chosen. For example, if there are two Microsoft + /// Entra accounts active and one Microsoft account, and consumers is chosen, silent + /// authentication works. + #[serde(alias = "invalid_resource", alias = "InvalidResource")] + InvalidResource, + + /// Too many or no users found. + /// The client requested silent authentication (prompt=none), but a single user couldn't be + /// found. This error may mean there are multiple users active in the session, or no users. + /// This error takes into account the tenant chosen. For example, if there are two Microsoft + /// Entra accounts active and one Microsoft account, and consumers is chosen, silent + /// authentication works. + #[serde(alias = "login_required", alias = "LoginRequired")] + LoginRequired, + + /// The request requires user interaction. + /// Another authentication step or consent is required. Retry the request without prompt=none. + #[serde(alias = "interaction_required", alias = "InteractionRequired")] + InteractionRequired, +} + +impl Display for AuthorizationQueryError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:#?}") + } } #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -56,6 +91,7 @@ pub struct AuthorizationQueryResponse { pub expires_in: Option<String>, pub access_token: Option<String>, pub state: Option<String>, + pub session_state: Option<String>, pub nonce: Option<String>, pub error: Option<AuthorizationQueryError>, pub error_description: Option<String>, @@ -75,6 +111,10 @@ impl AuthorizationQueryResponse { pub fn enable_pii_logging(&mut self, log_pii: bool) { self.log_pii = log_pii; } + + pub fn is_err(&self) -> bool { + self.error.is_some() + } } impl Debug for AuthorizationQueryResponse { diff --git a/graph-oauth/src/identity/authorization_serializer.rs b/graph-oauth/src/identity/authorization_url.rs similarity index 52% rename from graph-oauth/src/identity/authorization_serializer.rs rename to graph-oauth/src/identity/authorization_url.rs index 88008b95..6c9ed72e 100644 --- a/graph-oauth/src/identity/authorization_serializer.rs +++ b/graph-oauth/src/identity/authorization_url.rs @@ -1,18 +1,6 @@ -use std::collections::HashMap; - -use url::Url; - -use graph_error::IdentityResult; - use crate::identity::AzureCloudInstance; - -pub trait AuthorizationSerializer { - fn uri(&mut self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url>; - fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>>; - fn basic_auth(&self) -> Option<(String, String)> { - None - } -} +use graph_error::IdentityResult; +use url::Url; pub trait AuthorizationUrl { fn redirect_uri(&self) -> Option<&Url>; diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 9a0e6824..d23a51ad 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -82,7 +82,11 @@ impl TryFrom<ApplicationOptions> for AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), scope: Default::default(), - redirect_uri: None, + redirect_uri: Some( + Url::parse("http://localhost") + .map_err(|_| AF::msg_internal_err("redirect_uri")) + .unwrap(), + ), cache_id, force_token_refresh: Default::default(), log_pii: false, @@ -146,7 +150,11 @@ impl AppConfig { extra_query_parameters: Default::default(), extra_header_parameters: Default::default(), scope: Default::default(), - redirect_uri: None, + redirect_uri: Some( + Url::parse("http://localhost") + .map_err(|_| AF::msg_internal_err("redirect_uri")) + .unwrap(), + ), cache_id, force_token_refresh: Default::default(), log_pii: Default::default(), @@ -196,7 +204,14 @@ impl AppConfigBuilder { self } - pub fn build(self) -> AppConfig { + pub fn build(mut self) -> AppConfig { + if self.app_config.redirect_uri.is_none() { + self.app_config.redirect_uri = Some( + Url::parse("http://localhost") + .map_err(|_| AF::msg_internal_err("redirect_uri")) + .unwrap(), + ); + } self.app_config } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 025fa37e..60a2a766 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -1,6 +1,5 @@ use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; -use std::sync::mpsc::Receiver; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; @@ -18,7 +17,7 @@ use crate::identity::{ }; #[cfg(feature = "interactive-auth")] -use graph_error::{WebViewExecutionError, WebViewResult}; +use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResponse, Token}; @@ -26,7 +25,7 @@ use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResp #[cfg(feature = "interactive-auth")] use crate::web::{ HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, - WebViewOptions, WindowCloseReason, + WebViewOptions, }; #[cfg(feature = "interactive-auth")] @@ -35,8 +34,6 @@ use wry::{ webview::{WebView, WebViewBuilder}, }; -use crate::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; - credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// Get the authorization url required to perform the initial authorization and redirect in the @@ -192,17 +189,27 @@ impl AuthCodeAuthorizationUrlParameters { } #[cfg(feature = "interactive-auth")] + #[tracing::instrument] pub fn interactive_webview_authentication( &self, interactive_web_view_options: Option<WebViewOptions>, ) -> WebViewResult<AuthorizationQueryResponse> { - let uri = self.url()?; + let uri = self + .url() + .map_err(|err| Box::new(AuthExecutionError::from(err)))?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); - self.interactive_auth(uri, vec![redirect_uri], web_view_options, sender) + std::thread::spawn(move || { + AuthCodeAuthorizationUrlParameters::interactive_auth( + uri, + vec![redirect_uri], + web_view_options, + sender, + ) .unwrap(); + }); let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -214,106 +221,52 @@ impl AuthCodeAuthorizationUrlParameters { None => unreachable!(), Some(auth_event) => match auth_event { InteractiveAuthEvent::InvalidRedirectUri(reason) => { - Err(WebViewExecutionError::InvalidRedirectUri(reason)) + Err(WebViewError::InvalidUri(reason)) } InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let query = uri.query().or(uri.fragment()).ok_or( - WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), - )?; + let query = uri + .query() + .or(uri.fragment()) + .ok_or(WebViewError::InvalidUri(format!( + "uri missing query or fragment: {}", + uri.to_string() + )))?; let response_query: AuthorizationQueryResponse = - serde_urlencoded::from_str(query)?; - - Ok(response_query) - } - InteractiveAuthEvent::WindowClosed(window_close_reason) => Err( - WebViewExecutionError::WindowClosed(window_close_reason.to_string()), - ), - }, - } - } - - #[cfg(feature = "interactive-auth")] - pub fn interactive_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<Receiver<AuthCodeInteractiveEvent>> { - let uri = self.url()?; - let redirect_uri = self.redirect_uri().cloned().unwrap(); - let web_view_options = interactive_web_view_options.unwrap_or_default(); - let (sender, receiver) = std::sync::mpsc::channel(); - - self.interactive_auth(uri, vec![redirect_uri], web_view_options, sender) - .unwrap(); - let mut iter = receiver.try_iter(); - let mut next = iter.next(); - - while next.is_none() { - next = iter.next(); - } - - let (event_sender, event_receiver) = std::sync::mpsc::channel(); + serde_urlencoded::from_str(query) + .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; + + if response_query.is_err() { + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "error in authorization query or fragment from redirect uri"); + return Err(WebViewError::AuthorizationQuery { + error: response_query + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: response_query.error_description.unwrap_or_default(), + error_uri: response_query.error_uri.map(|uri| uri.to_string()), + }); + } - match next { - None => unreachable!(), - Some(auth_event) => match auth_event { - InteractiveAuthEvent::InvalidRedirectUri(reason) => { - return Err(WebViewExecutionError::InvalidRedirectUri(reason)); - } - InteractiveAuthEvent::ReachedRedirectUri(uri) => { - let query = uri.query().or(uri.fragment()).ok_or( - WebViewExecutionError::RedirectUriMissingQueryOrFragment(uri.to_string()), - )?; + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "parsed authorization query or fragment from redirect uri"); - let response_query: AuthorizationQueryResponse = - serde_urlencoded::from_str(query)?; - - event_sender - .send(AuthCodeInteractiveEvent::AuthorizationQuery(Box::new( - response_query.clone(), - ))) - .unwrap_or_default(); - - if let Some(code) = response_query.code.as_ref() { - let credential = AuthorizationCodeCredentialBuilder::new_with_auth_code( - self.app_config.clone(), - code, - ) - .build(); - event_sender - .send(AuthCodeInteractiveEvent::Success(credential)) - .unwrap_or_default(); - } + Ok(response_query) } InteractiveAuthEvent::WindowClosed(window_close_reason) => { - event_sender - .send(AuthCodeInteractiveEvent::WindowClosed(window_close_reason)) - .unwrap_or_default(); + Err(WebViewError::WindowClosed(window_close_reason.to_string())) } }, } - - Ok(event_receiver) } } -#[cfg(feature = "interactive-auth")] -#[derive(Debug)] -pub enum AuthCodeInteractiveEvent { - AuthorizationQuery(Box<AuthorizationQueryResponse>), - WindowClosed(WindowCloseReason), - Success(ConfidentialClientApplication<AuthorizationCodeCredential>), -} - #[cfg(feature = "interactive-auth")] impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { + #[tracing::instrument] fn webview( - &self, host_options: HostOptions, - _options: WebViewOptions, window: Window, proxy: EventLoopProxy<UserEvents>, - sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, ) -> anyhow::Result<WebView> { let start_uri = host_options.start_uri.clone(); let validator = WebViewHostValidator::try_from(host_options)?; @@ -327,20 +280,17 @@ impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { let is_redirect = validator.is_redirect_host(&url); if is_redirect { - sender - .send(InteractiveAuthEvent::ReachedRedirectUri(url.clone())) - .unwrap_or_default(); - // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. - - let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); + proxy.send_event(UserEvents::ReachedRedirectUri(url)) + .unwrap(); + proxy.send_event(UserEvents::InternalCloseWindow) + .unwrap(); return true; } is_valid_host } else { - tracing::debug!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); - let _ = proxy.send_event(UserEvents::CloseWindow); + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "unable to navigate webview - url is none"); + proxy.send_event(UserEvents::CloseWindow).unwrap(); false } }) @@ -348,39 +298,6 @@ impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { } } -#[cfg(feature = "interactive-auth")] -pub(crate) mod web_view_authenticator { - use crate::identity::{AuthCodeAuthorizationUrlParameters, AuthorizationUrl}; - use crate::web::{ - InteractiveAuthEvent, InteractiveAuthenticator, InteractiveWebView, WebViewOptions, - }; - use graph_error::WebViewResult; - - impl InteractiveAuthenticator for AuthCodeAuthorizationUrlParameters { - fn interactive_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>> { - let uri = self.url()?; - let redirect_uri = self.redirect_uri().cloned().unwrap(); - let web_view_options = interactive_web_view_options.unwrap_or_default(); - let (sender, receiver) = std::sync::mpsc::channel(); - - std::thread::spawn(move || { - InteractiveWebView::interactive_authentication( - uri, - vec![redirect_uri], - web_view_options, - sender, - ) - .unwrap(); - }); - - Ok(receiver) - } - } -} - impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { fn redirect_uri(&self) -> Option<&Url> { self.app_config.redirect_uri.as_ref() @@ -656,19 +573,28 @@ impl AuthCodeAuthorizationUrlParameterBuilder { pub fn with_interactive_authentication( &self, options: Option<WebViewOptions>, - ) -> WebViewResult<AuthorizationCodeCredentialBuilder> { + ) -> WebViewResult<( + AuthorizationQueryResponse, + AuthorizationCodeCredentialBuilder, + )> { let query_response = self .credential .interactive_webview_authentication(options)?; if let Some(authorization_code) = query_response.code.as_ref() { - Ok(AuthorizationCodeCredentialBuilder::new_with_auth_code( - self.credential.app_config.clone(), - authorization_code, + Ok(( + query_response.clone(), + AuthorizationCodeCredentialBuilder::new_with_auth_code( + self.credential.app_config.clone(), + authorization_code, + ), )) } else { - Ok(AuthorizationCodeCredentialBuilder::new_with_token( - self.credential.app_config.clone(), - Token::from(query_response), + Ok(( + query_response.clone(), + AuthorizationCodeCredentialBuilder::new_with_token( + self.credential.app_config.clone(), + Token::from(query_response), + ), )) } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 5c9a69bb..c27bef0a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -265,6 +265,7 @@ impl AuthorizationCodeCredentialBuilder { } } + #[cfg(feature = "interactive-auth")] pub(crate) fn new_with_token( app_config: AppConfig, token: Token, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 99185703..a4178d13 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -27,16 +27,19 @@ use crate::identity::{ }; #[cfg(feature = "interactive-auth")] -use crate::oauth::InteractiveDeviceCodeEvent; +use graph_error::WebViewDeviceCodeError; #[cfg(feature = "interactive-auth")] -use graph_error::WebViewResult; +use crate::web::WebViewOptions; #[cfg(feature = "interactive-auth")] -use crate::web::{InteractiveWebView, WebViewOptions, WindowCloseReason}; +use crate::web::{HostOptions, InteractiveAuth, UserEvents}; #[cfg(feature = "interactive-auth")] -use std::sync::mpsc::{Receiver, Sender}; +use wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, +}; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -349,15 +352,6 @@ impl DeviceCodeCredentialBuilder { } } -#[cfg(feature = "interactive-auth")] -#[derive(Debug)] -pub enum DeviceCodeInteractiveEvent { - DeviceAuthorization(DeviceAuthorizationResponse), - Failed(JsonHttpResponse), - WindowClosed(WindowCloseReason), - Success(PublicClientApplication<DeviceCodeCredential>), -} - #[derive(Debug)] pub struct DeviceCodePollingExecutor { credential: DeviceCodeCredential, @@ -547,15 +541,39 @@ impl DeviceCodePollingExecutor { } #[cfg(feature = "interactive-auth")] - pub fn execute_interactive_authentication( + pub fn with_interactive_authentication( &mut self, - ) -> AuthExecutionResult<DeviceCodeInteractiveAuth> { + ) -> AuthExecutionResult<(DeviceAuthorizationResponse, DeviceCodeInteractiveAuth)> { let response = self.credential.execute()?; let device_authorization_response: DeviceAuthorizationResponse = response.json()?; - Ok(DeviceCodeInteractiveAuth { - credential: self.credential.clone(), - device_authorization_response, - }) + Ok(( + device_authorization_response.clone(), + DeviceCodeInteractiveAuth { + credential: self.credential.clone(), + device_authorization_response, + }, + )) + } +} + +#[cfg(feature = "interactive-auth")] +impl InteractiveAuth for DeviceCodeCredential { + fn webview( + host_options: HostOptions, + window: Window, + _proxy: EventLoopProxy<UserEvents>, + ) -> anyhow::Result<WebView> { + Ok(WebViewBuilder::new(window)? + .with_url(host_options.start_uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + tracing::event!(tracing::Level::INFO, url = url.as_str()); + } + true + }) + .build()?) } } @@ -579,212 +597,104 @@ impl DeviceCodeInteractiveAuth { } } - pub fn begin( + pub fn interactive_webview_authentication( &mut self, options: Option<WebViewOptions>, - ) -> WebViewResult<Receiver<DeviceCodeInteractiveEvent>> { - let executor = self.interactive_webview_authentication(options)?; - let (sender, receiver) = std::sync::mpsc::channel(); - - std::thread::spawn(move || { - DeviceCodeInteractiveAuth::execute_interactive_loop(sender, executor); - }); - - Ok(receiver) - } - - #[tracing::instrument] - fn execute_interactive_loop( - sender: Sender<DeviceCodeInteractiveEvent>, - executor: Receiver<InteractiveDeviceCodeEvent>, - ) { - loop { - match executor.recv() { - Ok(interactive_device_code_event) => match interactive_device_code_event { - InteractiveDeviceCodeEvent::PollDeviceCode { - poll_device_code_event, - response, - } => { - let res = response.json().unwrap_or_default().to_string(); - tracing::debug!(target: "device_code_polling_executor", poll_device_code = poll_device_code_event.as_str(), http_response = res); - - match poll_device_code_event { - PollDeviceCodeEvent::AuthorizationPending - | PollDeviceCodeEvent::SlowDown => continue, - PollDeviceCodeEvent::AuthorizationDeclined - | PollDeviceCodeEvent::BadVerificationCode - | PollDeviceCodeEvent::ExpiredToken - | PollDeviceCodeEvent::AccessDenied => { - sender - .send(DeviceCodeInteractiveEvent::Failed(response)) - .unwrap_or_default(); - break; - } - } - } - InteractiveDeviceCodeEvent::WindowClosed(window_closed) => { - sender - .send(DeviceCodeInteractiveEvent::WindowClosed(window_closed)) - .unwrap_or_default(); - break; - } - InteractiveDeviceCodeEvent::SuccessfulAuthEvent { - response: _, - public_application, - } => { - tracing::debug!(target: "device_code_polling_executor", "PublicApplication: {public_application:#?}"); - sender - .send(DeviceCodeInteractiveEvent::Success(public_application)) - .unwrap_or_default(); - } - _ => {} - }, - Err(err) => panic!("{}", err), + ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { + let url = { + if let Some(url_complete) = self + .device_authorization_response + .verification_uri_complete + .as_ref() + { + Url::parse(url_complete).unwrap() + } else { + Url::parse(self.device_authorization_response.verification_uri.as_str()).unwrap() } - } - } + }; - #[tracing::instrument] - pub fn interactive_webview_authentication( - &mut self, - options: Option<WebViewOptions>, - ) -> WebViewResult<Receiver<InteractiveDeviceCodeEvent>> { - let (sender, receiver) = std::sync::mpsc::channel(); - let mut credential = self.credential.clone(); - let device_authorization_response = self.device_authorization_response.clone(); + let (sender, _receiver) = std::sync::mpsc::channel(); - // Spawn thread for webview - let sender3 = sender.clone(); std::thread::spawn(move || { - let url = { - if let Some(url_complete) = device_authorization_response - .verification_uri_complete - .as_ref() - { - Url::parse(url_complete).unwrap() - } else { - Url::parse(device_authorization_response.verification_uri.as_str()).unwrap() - } - }; - - InteractiveWebView::device_code_interactive_authentication( + DeviceCodeCredential::interactive_auth( url, + vec![], options.unwrap_or_default(), - sender3, + sender, ) .unwrap(); }); - let device_code = device_authorization_response.device_code; - let interval = Duration::from_secs(device_authorization_response.interval); - credential.with_device_code(device_code); + self.poll() + } - let sender2 = sender; - std::thread::spawn(move || { - let mut should_slow_down = false; + #[tracing::instrument] + pub(crate) fn poll( + &mut self, + ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { + let mut credential = self.credential.clone(); - loop { - // Wait the amount of seconds that interval is. - if should_slow_down { - should_slow_down = false; - std::thread::sleep(interval.add(Duration::from_secs(5))); - } else { - std::thread::sleep(interval); - } + let device_code = self.device_authorization_response.device_code.clone(); + let interval = Duration::from_secs(self.device_authorization_response.interval); + credential.with_device_code(device_code); - let response = credential.execute().unwrap(); - tracing::debug!(target: "device_code_polling_executor", "{response:#?}"); - let http_response = response.into_http_response()?; - let status = http_response.status(); + let mut should_slow_down = false; - if status.is_success() { - let json = http_response.json().unwrap(); - let token: Token = serde_json::from_value(json)?; - let cache_id = credential.app_config.cache_id.clone(); - credential.token_cache.store(cache_id, token); - sender2.send(InteractiveDeviceCodeEvent::SuccessfulAuthEvent { - response: http_response, - public_application: PublicClientApplication::from(credential), - })?; - break; - } else { - let json = http_response.json().unwrap(); - let option_error = json["error"].as_str().map(|value| value.to_owned()); + loop { + // Wait the amount of seconds that interval is. + if should_slow_down { + should_slow_down = false; + std::thread::sleep(interval.add(Duration::from_secs(5))); + } else { + std::thread::sleep(interval); + } - if let Some(error) = option_error { - match PollDeviceCodeEvent::from_str(error.as_str()) { - Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeEvent::AuthorizationPending => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::AuthorizationPending, - })?; - continue; - } - PollDeviceCodeEvent::AuthorizationDeclined => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::AuthorizationDeclined, - })?; - break; - } - PollDeviceCodeEvent::BadVerificationCode => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: - PollDeviceCodeEvent::BadVerificationCode, - })?; - continue; - } - PollDeviceCodeEvent::ExpiredToken => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: PollDeviceCodeEvent::ExpiredToken, - })?; - break; - } - PollDeviceCodeEvent::AccessDenied => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: PollDeviceCodeEvent::AccessDenied, - })?; - break; - } - PollDeviceCodeEvent::SlowDown => { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: PollDeviceCodeEvent::SlowDown, - })?; + let response = credential.execute().unwrap(); + let http_response = response.into_http_response()?; + let status = http_response.status(); + + if status.is_success() { + let json = http_response.json().unwrap(); + let token: Token = + serde_json::from_value(json).map_err(AuthExecutionError::from)?; + let cache_id = credential.app_config.cache_id.clone(); + credential.token_cache.store(cache_id, token); + return Ok(PublicClientApplication::from(credential)); + } else { + let json = http_response.json().unwrap(); + let option_error = json["error"].as_str().map(|value| value.to_owned()); - should_slow_down = true; - continue; - } - }, - Err(err) => { - tracing::trace!(target: "device_code_polling_executor", "Error occurred while polling device code: {err:#?}"); - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: PollDeviceCodeEvent::AccessDenied, - })?; - break; + if let Some(error) = option_error { + match PollDeviceCodeEvent::from_str(error.as_str()) { + Ok(poll_device_code_type) => match poll_device_code_type { + PollDeviceCodeEvent::AuthorizationPending + | PollDeviceCodeEvent::BadVerificationCode => continue, + PollDeviceCodeEvent::SlowDown => { + should_slow_down = true; + continue; } + PollDeviceCodeEvent::AuthorizationDeclined + | PollDeviceCodeEvent::ExpiredToken + | PollDeviceCodeEvent::AccessDenied => { + return Err(WebViewDeviceCodeError::DeviceCodePollingError( + http_response, + )); + } + }, + Err(_) => { + return Err(WebViewDeviceCodeError::DeviceCodePollingError( + http_response, + )); } - } else { - sender2.send(InteractiveDeviceCodeEvent::PollDeviceCode { - response: http_response, - poll_device_code_event: PollDeviceCodeEvent::AccessDenied, - })?; - // Body should have error or we should bail. - break; } + } else { + // Body should have error or we should bail. + return Err(WebViewDeviceCodeError::DeviceCodePollingError( + http_response, + )); } } - Ok::<(), anyhow::Error>(()) - }); - - Ok(receiver) + } } } diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index fee07c07..273cde14 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -15,6 +15,24 @@ use crate::identity::{ AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; +#[cfg(feature = "interactive-auth")] +use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; + +#[cfg(feature = "interactive-auth")] +use crate::identity::{AuthorizationQueryResponse, OpenIdCredentialBuilder, Token}; + +#[cfg(feature = "interactive-auth")] +use crate::web::{ + HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, + WebViewOptions, +}; + +#[cfg(feature = "interactive-auth")] +use wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, +}; + const RESPONSE_TYPES_SUPPORTED: &[&str] = &["code", "id_token", "code id_token", "id_token token"]; /// OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional @@ -112,6 +130,7 @@ impl Debug for OpenIdAuthorizationUrlParameters { .finish() } } + impl OpenIdAuthorizationUrlParameters { pub fn new<T: AsRef<str>, IU: IntoUrl, U: ToString, I: IntoIterator<Item = U>>( client_id: T, @@ -183,6 +202,77 @@ impl OpenIdAuthorizationUrlParameters { pub fn nonce(&mut self) -> &String { &self.nonce } + + #[cfg(feature = "interactive-auth")] + #[tracing::instrument] + pub fn interactive_webview_authentication( + &self, + interactive_web_view_options: Option<WebViewOptions>, + ) -> WebViewResult<AuthorizationQueryResponse> { + let uri = self + .url() + .map_err(|err| Box::new(AuthExecutionError::from(err)))?; + let redirect_uri = self.redirect_uri().cloned().unwrap(); + let web_view_options = interactive_web_view_options.unwrap_or_default(); + let (sender, receiver) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + OpenIdAuthorizationUrlParameters::interactive_auth( + uri, + vec![redirect_uri], + web_view_options, + sender, + ) + .unwrap(); + }); + let mut iter = receiver.try_iter(); + let mut next = iter.next(); + + while next.is_none() { + next = iter.next(); + } + + match next { + None => unreachable!(), + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(WebViewError::InvalidUri(reason)) + } + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let query = uri + .query() + .or(uri.fragment()) + .ok_or(WebViewError::InvalidUri(format!( + "uri missing query or fragment: {}", + uri.to_string() + )))?; + + let response_query: AuthorizationQueryResponse = + serde_urlencoded::from_str(query) + .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; + + if response_query.is_err() { + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "error in authorization query or fragment from redirect uri"); + return Err(WebViewError::AuthorizationQuery { + error: response_query + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: response_query.error_description.unwrap_or_default(), + error_uri: response_query.error_uri.map(|uri| uri.to_string()), + }); + } + + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "parsed authorization query or fragment from redirect uri"); + + Ok(response_query) + } + InteractiveAuthEvent::WindowClosed(window_close_reason) => { + Err(WebViewError::WindowClosed(window_close_reason.to_string())) + } + }, + } + } } impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { @@ -205,10 +295,6 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { return AuthorizationFailure::result("client_id"); } - if self.app_config.scope.is_empty() { - return AuthorizationFailure::result("scope"); - } - if self.nonce.is_empty() { return AuthorizationFailure::msg_result( "nonce", @@ -216,9 +302,16 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { ); } + if self.app_config.scope.is_empty() || !self.app_config.scope.contains("openid") { + let mut scope = self.app_config.scope.clone(); + scope.insert("openid".into()); + serializer.set_scope(scope); + } else { + serializer.set_scope(self.app_config.scope.clone()); + } + serializer .client_id(client_id.as_str()) - .set_scope(self.app_config.scope.clone()) .nonce(self.nonce.as_str()) .authority(azure_cloud_instance, &self.app_config.authority); @@ -293,8 +386,46 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { } } +#[cfg(feature = "interactive-auth")] +impl InteractiveAuth for OpenIdAuthorizationUrlParameters { + #[tracing::instrument] + fn webview( + host_options: HostOptions, + window: Window, + proxy: EventLoopProxy<UserEvents>, + ) -> anyhow::Result<WebView> { + let start_uri = host_options.start_uri.clone(); + let validator = WebViewHostValidator::try_from(host_options)?; + Ok(WebViewBuilder::new(window)? + .with_url(start_uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + let is_valid_host = validator.is_valid_uri(&url); + let is_redirect = validator.is_redirect_host(&url); + + if is_redirect { + proxy.send_event(UserEvents::ReachedRedirectUri(url)) + .unwrap(); + proxy.send_event(UserEvents::InternalCloseWindow) + .unwrap(); + return true; + } + + is_valid_host + } else { + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "unable to navigate webview - url is none"); + proxy.send_event(UserEvents::CloseWindow).unwrap(); + false + } + }) + .build()?) + } +} + pub struct OpenIdAuthorizationUrlParameterBuilder { - parameters: OpenIdAuthorizationUrlParameters, + credential: OpenIdAuthorizationUrlParameters, } impl OpenIdAuthorizationUrlParameterBuilder { @@ -302,7 +433,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { client_id: impl AsRef<str>, ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { Ok(OpenIdAuthorizationUrlParameterBuilder { - parameters: OpenIdAuthorizationUrlParameters::new_with_app_config( + credential: OpenIdAuthorizationUrlParameters::new_with_app_config( AppConfig::builder(client_id.as_ref()) .scope(vec!["openid"]) .build(), @@ -311,10 +442,11 @@ impl OpenIdAuthorizationUrlParameterBuilder { } pub(crate) fn new_with_app_config( - app_config: AppConfig, + mut app_config: AppConfig, ) -> OpenIdAuthorizationUrlParameterBuilder { + app_config.scope.insert("openid".into()); OpenIdAuthorizationUrlParameterBuilder { - parameters: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config) + credential: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config) .expect("ring::crypto::Unspecified"), } } @@ -323,24 +455,24 @@ impl OpenIdAuthorizationUrlParameterBuilder { &mut self, redirect_uri: T, ) -> IdentityResult<&mut Self> { - self.parameters.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); + self.credential.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); Ok(self) } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.parameters.app_config.client_id = + self.credential.app_config.client_id = Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.parameters.app_config.authority = authority.into(); + self.credential.app_config.authority = authority.into(); self } @@ -358,7 +490,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { &mut self, response_type: I, ) -> &mut Self { - self.parameters.response_type = BTreeSet::from_iter(response_type.into_iter()); + self.credential.response_type = BTreeSet::from_iter(response_type.into_iter()); self } @@ -371,7 +503,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// - **form_post**: Executes a POST containing the code to your redirect URI. /// Supported when requesting a code. pub fn with_response_mode(&mut self, response_mode: ResponseMode) -> &mut Self { - self.parameters.response_mode = Some(response_mode); + self.credential.response_mode = Some(response_mode); self } @@ -386,27 +518,27 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// authorization code grant. If you are unsure or unclear how the nonce works then it is /// recommended to stay with the generated nonce as it is cryptographically secure. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - if self.parameters.nonce.is_empty() { - self.parameters.nonce.push_str(nonce.as_ref()); + if self.credential.nonce.is_empty() { + self.credential.nonce.push_str(nonce.as_ref()); } else { - self.parameters.nonce = nonce.as_ref().to_owned(); + self.credential.nonce = nonce.as_ref().to_owned(); } self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.parameters.state = Some(state.as_ref().to_owned()); + self.credential.state = Some(state.as_ref().to_owned()); self } /// Takes an iterator of scopes to use in the request. /// Replaces current scopes if any were added previously. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - if self.parameters.app_config.scope.contains("offline_access") { - self.parameters.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + if self.credential.app_config.scope.contains("offline_access") { + self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); self.with_offline_access(); } else { - self.parameters.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); } self } @@ -414,7 +546,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// Adds the `offline_access` scope parameter which tells the authorization server /// to include a refresh token in the response. pub fn with_offline_access(&mut self) -> &mut Self { - self.parameters + self.credential .app_config .scope .extend(vec!["offline_access".to_owned()]); @@ -432,7 +564,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self { - self.parameters.prompt.extend(prompt.into_iter()); + self.credential.prompt.extend(prompt.into_iter()); self } @@ -442,7 +574,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// user experience. For tenants that are federated through an on-premises directory /// like AD FS, this often results in a seamless sign-in because of the existing login session. pub fn with_domain_hint<T: AsRef<str>>(&mut self, domain_hint: T) -> &mut Self { - self.parameters.domain_hint = Some(domain_hint.as_ref().to_owned()); + self.credential.domain_hint = Some(domain_hint.as_ref().to_owned()); self } @@ -452,16 +584,43 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// this parameter during reauthentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub fn with_login_hint<T: AsRef<str>>(&mut self, login_hint: T) -> &mut Self { - self.parameters.login_hint = Some(login_hint.as_ref().to_owned()); + self.credential.login_hint = Some(login_hint.as_ref().to_owned()); self } + #[cfg(feature = "interactive-auth")] + pub fn with_interactive_authentication( + &self, + options: Option<WebViewOptions>, + ) -> WebViewResult<(AuthorizationQueryResponse, OpenIdCredentialBuilder)> { + let query_response = self + .credential + .interactive_webview_authentication(options)?; + if let Some(authorization_code) = query_response.code.as_ref() { + Ok(( + query_response.clone(), + OpenIdCredentialBuilder::new_with_auth_code( + self.credential.app_config.clone(), + authorization_code, + ), + )) + } else { + Ok(( + query_response.clone(), + OpenIdCredentialBuilder::new_with_token( + self.credential.app_config.clone(), + Token::from(query_response.clone()), + ), + )) + } + } + pub fn build(&self) -> OpenIdAuthorizationUrlParameters { - self.parameters.clone() + self.credential.clone() } pub fn url(&self) -> IdentityResult<Url> { - self.parameters.url() + self.credential.url() } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index be86359c..4f6118c5 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -368,6 +368,26 @@ impl OpenIdCredentialBuilder { } } + #[cfg(feature = "interactive-auth")] + pub(crate) fn new_with_auth_code( + mut app_config: AppConfig, + authorization_code: impl AsRef<str>, + ) -> OpenIdCredentialBuilder { + app_config.scope.insert("openid".to_string()); + OpenIdCredentialBuilder { + credential: OpenIdCredential { + app_config, + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: Default::default(), + code_verifier: None, + pkce: None, + serializer: Default::default(), + token_cache: Default::default(), + }, + } + } + pub(crate) fn new_with_auth_code_and_secret( authorization_code: impl AsRef<str>, client_secret: impl AsRef<str>, @@ -388,6 +408,26 @@ impl OpenIdCredentialBuilder { } } + #[cfg(feature = "interactive-auth")] + pub(crate) fn new_with_token(app_config: AppConfig, token: Token) -> OpenIdCredentialBuilder { + let cache_id = app_config.cache_id.clone(); + let mut token_cache = InMemoryCacheStore::new(); + token_cache.store(cache_id, token); + + Self { + credential: OpenIdCredential { + app_config, + authorization_code: None, + refresh_token: None, + client_secret: Default::default(), + code_verifier: None, + pkce: None, + serializer: Default::default(), + token_cache, + }, + } + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self.credential.refresh_token = None; diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 6ec027b2..1324fde1 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -39,7 +39,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { } #[tracing::instrument] - fn build(&mut self) -> AuthExecutionResult<reqwest::blocking::RequestBuilder> { + fn build_request(&mut self) -> AuthExecutionResult<reqwest::blocking::RequestBuilder> { let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) @@ -56,7 +56,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - "authorization request constructed; request_builder={request_builder:#?}" + target: "graph_rs_sdk::token_credential_executor", + "authorization request constructed" ); Ok(request_builder) } else { @@ -66,14 +67,15 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - "authorization request constructed; request_builder={request_builder:#?}" + target: "graph_rs_sdk::token_credential_executor", + "authorization request constructed" ); Ok(request_builder) } } #[tracing::instrument] - fn build_async(&mut self) -> AuthExecutionResult<reqwest::RequestBuilder> { + fn build_request_async(&mut self) -> AuthExecutionResult<reqwest::RequestBuilder> { let http_client = reqwest::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) .https_only(true) @@ -90,7 +92,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - "authorization request constructed; request_builder={request_builder:#?}" + target: "graph_rs_sdk::token_credential_executor", + "authorization request constructed" ); Ok(request_builder) } else { @@ -100,7 +103,8 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - "authorization request constructed; request_builder={request_builder:#?}" + target: "graph_rs_sdk::token_credential_executor", + "authorization request constructed" ); Ok(request_builder) } @@ -134,17 +138,19 @@ pub trait TokenCredentialExecutor: DynClone + Debug { #[tracing::instrument] fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { - let request_builder = self.build()?; + let request_builder = self.build_request()?; let response = request_builder.send()?; - tracing::debug!("authorization response received; response={response:#?}"); + let status = response.status(); + tracing::debug!(target: "graph_rs_sdk::token_credential_executor", "authorization response received; status={status:#?}"); Ok(response) } #[tracing::instrument] async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { - let request_builder = self.build_async()?; + let request_builder = self.build_request_async()?; let response = request_builder.send().await?; - tracing::debug!("authorization response received; response={response:#?}"); + let status = response.status(); + tracing::debug!(target: "graph_rs_sdk::token_credential_executor", "authorization response received; status={status:#?}"); Ok(response) } } diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index fa886d55..8d5c4162 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -4,7 +4,7 @@ mod authority; mod authorization_query_response; mod authorization_request_parts; mod authorization_response; -mod authorization_serializer; +mod authorization_url; mod credentials; mod device_authorization_response; @@ -24,7 +24,7 @@ pub use authority::*; pub use authorization_query_response::*; pub use authorization_request_parts::*; pub use authorization_response::*; -pub use authorization_serializer::*; +pub use authorization_url::*; pub use credentials::*; pub use device_authorization_response::*; pub use id_token::*; diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index b8d5fbf4..dccd24f5 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -6,10 +6,12 @@ use std::time::{Duration, Instant}; use url::Url; use wry::application::event::{Event, StartCause, WindowEvent}; use wry::application::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; -use wry::application::platform::windows::EventLoopBuilderExtWindows; use wry::application::window::{Window, WindowBuilder}; use wry::webview::WebView; +#[cfg(target_family = "windows")] +use wry::application::platform::windows::EventLoopBuilderExtWindows; + pub trait InteractiveAuthenticator { fn interactive_authentication( &self, @@ -22,17 +24,13 @@ where Self: Debug, { fn webview( - &self, host_options: HostOptions, - options: WebViewOptions, window: Window, proxy: EventLoopProxy<UserEvents>, - sender: Sender<InteractiveAuthEvent>, ) -> anyhow::Result<WebView>; #[tracing::instrument] fn interactive_auth( - &self, start_url: Url, redirect_uris: Vec<Url>, options: WebViewOptions, @@ -41,15 +39,8 @@ where let event_loop: EventLoop<UserEvents> = Self::event_loop(); let proxy = event_loop.create_proxy(); let window = Self::window_builder(&options).build(&event_loop).unwrap(); - - let webview_options = options.clone(); - let webview = self.webview( - HostOptions::new(start_url, redirect_uris, options.ports.clone()), - webview_options, - window, - proxy, - sender.clone(), - )?; + let host_options = HostOptions::new(start_url, redirect_uris, options.ports.clone()); + let webview = Self::webview(host_options, window, proxy)?; event_loop.run(move |event, _, control_flow| { if let Some(timeout) = options.timeout.as_ref() { @@ -59,7 +50,7 @@ where } match event { - Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), + Event::NewEvents(StartCause::Init) => tracing::trace!(target: "interactive_webview", "Webview runtime started"), Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { start, requested_resume @@ -79,7 +70,7 @@ where .. } => { sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); - tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); + tracing::trace!(target: "interactive_webview", "Window close requested by user"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); @@ -90,8 +81,12 @@ where *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); - + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri}"); + sender.send(InteractiveAuthEvent::ReachedRedirectUri(uri)) + .unwrap_or_default(); + } + Event::UserEvent(UserEvents::InternalCloseWindow) => { + tracing::trace!(target: "interactive_webview", "Closing window"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); } @@ -106,6 +101,7 @@ where }); } + #[cfg(target_family = "windows")] fn window_builder(options: &WebViewOptions) -> WindowBuilder { WindowBuilder::new() .with_title(options.window_title.clone()) @@ -118,6 +114,18 @@ where .with_theme(options.theme) } + #[cfg(target_family = "unix")] + fn window_builder(options: &WebViewOptions) -> WindowBuilder { + WindowBuilder::new() + .with_title(options.window_title.clone()) + .with_closable(true) + .with_content_protection(true) + .with_minimizable(true) + .with_maximizable(true) + .with_focused(true) + .with_resizable(true) + } + #[cfg(target_family = "windows")] fn event_loop() -> EventLoop<UserEvents> { EventLoopBuilder::with_user_event() diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs index cd863e5a..d9c604b7 100644 --- a/graph-oauth/src/web/interactive_web_view.rs +++ b/graph-oauth/src/web/interactive_web_view.rs @@ -4,7 +4,7 @@ use url::Url; use crate::oauth::InteractiveDeviceCodeEvent; use crate::web::{HostOptions, InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; -use graph_error::{WebViewExecutionError, WebViewResult}; +use graph_error::{WebViewError, WebViewResult}; use wry::application::event_loop::EventLoopBuilder; use wry::application::platform::windows::EventLoopBuilderExtWindows; use wry::{ @@ -19,6 +19,7 @@ use wry::{ #[derive(Debug, Clone)] pub enum UserEvents { CloseWindow, + InternalCloseWindow, ReachedRedirectUri(Url), } @@ -36,9 +37,9 @@ impl WebViewHostValidator { ports: HashSet<usize>, ) -> WebViewResult<WebViewHostValidator> { if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { - return Err(WebViewExecutionError::InvalidStartUri { - reason: "Authorization url and redirect uri must have valid uri hosts".to_owned(), - }); + return Err(WebViewError::InvalidUri( + "Authorization url and redirect uri must have valid uri hosts".into(), + )); } let is_local_host = redirect_uris @@ -46,9 +47,9 @@ impl WebViewHostValidator { .any(|uri| uri.as_str().eq("http://localhost")); if is_local_host && ports.is_empty() { - return Err(WebViewExecutionError::InvalidStartUri { - reason: "Redirect uri is http://localhost but not ports were specified".to_string(), - }); + return Err(WebViewError::InvalidUri( + "Redirect uri is http://localhost but not ports were specified".into(), + )); } Ok(WebViewHostValidator { @@ -99,7 +100,7 @@ impl WebViewHostValidator { } impl TryFrom<HostOptions> for WebViewHostValidator { - type Error = WebViewExecutionError; + type Error = WebViewError; fn try_from(value: HostOptions) -> Result<Self, Self::Error> { WebViewHostValidator::new(value.start_uri, value.redirect_uris, value.ports) @@ -214,6 +215,18 @@ impl InteractiveWebView { std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } + Event::UserEvent(UserEvents::InternalCloseWindow) => { + tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); + + if options.clear_browsing_data { + let _ = webview.clear_all_browsing_data(); + } + + // Wait time to avoid deadlock where window closes before + // the channel has received the redirect uri. + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } _ => (), } }); diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/web_view_options.rs index 491eeca7..441d0145 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/web_view_options.rs @@ -4,6 +4,7 @@ use url::Url; pub use wry::application::window::Theme; +#[derive(Debug)] pub struct HostOptions { pub(crate) start_uri: Url, pub(crate) redirect_uris: Vec<Url>, diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 17dfc562..0ddeb6a6 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -224,7 +224,7 @@ impl OAuthTestClient { } pub fn request_access_token(&self) -> Option<(String, Token)> { - if Environment::is_local() || Environment::is_travis() { + if Environment::is_local() || Environment::is_travis() || Environment::is_github() { let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, @@ -232,25 +232,13 @@ impl OAuthTestClient { self.get_access_token(test_client_map.get(self).unwrap()) } else if Environment::is_appveyor() { self.get_access_token(OAuthTestCredentials::new_env()) - } else if Environment::is_github() { - let map = OAuthTestClient::get_app_registration()?; - let test_client_map = OAuthTestClientMap { - clients: map.get_default_client_credentials().clients, - }; - self.get_access_token(test_client_map.get(self).unwrap()) } else { None } } pub fn request_access_token_credential(&self) -> Option<(String, impl ClientApplication)> { - if Environment::is_local() || Environment::is_travis() { - let map = OAuthTestClient::get_app_registration()?; - let test_client_map = OAuthTestClientMap { - clients: map.get_default_client_credentials().clients, - }; - self.get_credential(test_client_map.get(self).unwrap()) - } else if Environment::is_github() { + if Environment::is_local() || Environment::is_travis() || Environment::is_github() { let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, @@ -262,7 +250,7 @@ impl OAuthTestClient { } pub async fn request_access_token_async(&self) -> Option<(String, Token)> { - if Environment::is_local() || Environment::is_travis() { + if Environment::is_local() || Environment::is_travis() || Environment::is_github() { let map = OAuthTestClient::get_app_registration()?; let test_client_map = OAuthTestClientMap { clients: map.get_default_client_credentials().clients, @@ -272,13 +260,6 @@ impl OAuthTestClient { } else if Environment::is_appveyor() { self.get_access_token_async(OAuthTestCredentials::new_env()) .await - } else if Environment::is_github() { - let map = OAuthTestClient::get_app_registration()?; - let test_client_map = OAuthTestClientMap { - clients: map.get_default_client_credentials().clients, - }; - self.get_access_token_async(test_client_map.get(self).unwrap()) - .await } else { None } From 7484983dad1a6edd83bba17114a4be531fad0bb0 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 16 Nov 2023 02:04:11 -0500 Subject: [PATCH 063/118] Update serializer and use of serializer in credential builders --- README.md | 2 + .../oauth_authorization_url/openid_connect.rs | 41 ++- .../auth_code_authorization_url.rs | 39 +-- ...authorization_code_assertion_credential.rs | 21 +- ...thorization_code_certificate_credential.rs | 20 +- .../authorization_code_credential.rs | 27 +- .../client_assertion_credential.rs | 11 +- .../client_certificate_credential.rs | 11 +- .../client_credentials_authorization_url.rs | 2 +- .../credentials/client_secret_credential.rs | 12 +- .../credentials/device_code_credential.rs | 20 +- .../credentials/legacy/implicit_credential.rs | 101 +++---- .../credentials/open_id_authorization_url.rs | 118 ++++---- .../credentials/open_id_credential.rs | 1 - .../resource_owner_password_credential.rs | 16 +- graph-oauth/src/jwt.rs | 260 ----------------- graph-oauth/src/lib.rs | 8 +- graph-oauth/src/oauth_error.rs | 57 ---- .../src/{auth.rs => oauth_serializer.rs} | 264 ++---------------- tests/jwt_tests.rs | 68 ----- 20 files changed, 242 insertions(+), 857 deletions(-) delete mode 100644 graph-oauth/src/jwt.rs delete mode 100644 graph-oauth/src/oauth_error.rs rename graph-oauth/src/{auth.rs => oauth_serializer.rs} (76%) delete mode 100644 tests/jwt_tests.rs diff --git a/README.md b/README.md index bf6135f5..4f3afc03 100644 --- a/README.md +++ b/README.md @@ -943,6 +943,8 @@ async fn get_user() -> GraphResult<()> { ## OAuth - Getting Access Tokens + +### Warning The crate is undergoing major development in order to support all or most scenarios in the Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some unstable features. Any version that is not a pre-release version of the crate is considered stable. diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index 6fef6e65..e68399d9 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -1,7 +1,8 @@ -use graph_error::IdentityResult; +use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ ConfidentialClientApplication, OpenIdCredential, Prompt, ResponseMode, ResponseType, }; +use graph_rs_sdk::GraphClient; use url::Url; // The authorization request is the initial request to sign in where the user @@ -12,6 +13,7 @@ use url::Url; // If you are listening on a server use the response mod ResponseMode::FormPost. // Servers do not get sent the URL query and so in order to get what would normally be in // the query of URL use a form post which sends the data as a POST http request. +// Furthermore openid does not support the query response mode but does support fragment. // The URL builder below will create the full URL with the query that you will // need to send the user to such as redirecting the page they are on when using @@ -40,7 +42,40 @@ fn openid_authorization_url3( .build() .url() } -fn open_id_authorization_url( + +fn map_to_credential( + client_id: &str, + tenant: &str, + redirect_uri: &str, + state: &str, + scope: Vec<&str>, + client_secret: &str, +) { + let auth_url_builder = OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant) + //.with_default_scope()? + .with_redirect_uri(redirect_uri)? + .with_response_mode(ResponseMode::FormPost) + .with_response_type([ResponseType::IdToken, ResponseType::Code]) + .with_prompt(Prompt::SelectAccount) + .with_state(state) + .with_scope(scope); + + // Open the url in a web browser, sign in, and get the authorization code + // returned in the POST to the redirect uri. + let _url = auth_url_builder.url().unwrap(); + + // Code returned on redirect uri. + let authorization_code = "..."; + + // Use the authorization url builder to create the credential builder. + let mut credential_builder = auth_url_builder.into_credential(authorization_code); + let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + + let _graph_client = GraphClient::from(&confidential_client); +} + +fn auth_url_using_confidential_client_builder( client_id: &str, tenant: &str, redirect_uri: &str, @@ -56,7 +91,7 @@ fn open_id_authorization_url( } // Same as above -fn open_id_authorization_url2( +fn auth_url_using_open_id_credential( client_id: &str, tenant: &str, redirect_uri: &str, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 60a2a766..cc4bbadc 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -10,11 +10,11 @@ use uuid::Uuid; use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; use graph_error::{IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ - credentials::app_config::AppConfig, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, - ResponseType, + credentials::app_config::AppConfig, AsQuery, AuthorizationUrl, AzureCloudInstance, Prompt, + ResponseMode, ResponseType, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; #[cfg(feature = "interactive-auth")] use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; @@ -73,7 +73,8 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); pub struct AuthCodeAuthorizationUrlParameters { pub(crate) app_config: AppConfig, pub(crate) response_type: BTreeSet<ResponseType>, - /// Optional + /// Optional (recommended) + /// /// Specifies how the identity platform should return the requested token to your app. /// /// Supported values: @@ -113,7 +114,7 @@ pub struct AuthCodeAuthorizationUrlParameters { /// Finally, [Prompt::SelectAccount] shows the user an account selector, negating silent SSO but /// allowing the user to pick which account they intend to sign in with, without requiring /// credential entry. You can't use both login_hint and select_account. - pub(crate) prompt: Option<Prompt>, + pub(crate) prompt: BTreeSet<Prompt>, /// Optional /// The realm of the user in a federated directory. This skips the email-based discovery /// process that the user goes through on the sign-in page, for a slightly more streamlined @@ -158,7 +159,7 @@ impl AuthCodeAuthorizationUrlParameters { response_mode: None, nonce: None, state: None, - prompt: None, + prompt: Default::default(), domain_hint: None, login_hint: None, code_challenge: None, @@ -332,8 +333,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { serializer .client_id(client_id.as_str()) - .set_scope(self.app_config.scope.clone()) - .authority(azure_cloud_instance, &self.app_config.authority); + .set_scope(self.app_config.scope.clone()); let response_types: Vec<String> = self.response_type.iter().map(|s| s.to_string()).collect(); @@ -353,9 +353,11 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { // Set response_mode if self.response_type.contains(&ResponseType::IdToken) { - if self.response_mode.is_none() || self.response_mode.eq(&Some(ResponseMode::Query)) - { - serializer.response_mode(ResponseMode::Fragment.as_ref()); + if self.response_mode.eq(&Some(ResponseMode::Query)) { + return Err(AF::msg_err( + "response_mode", + "ResponseType::IdToken requires ResponseMode::Fragment or ResponseMode::FormPost") + ); } else if let Some(response_mode) = self.response_mode.as_ref() { serializer.response_mode(response_mode.as_ref()); } @@ -368,8 +370,8 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { serializer.state(state.as_str()); } - if let Some(prompt) = self.prompt.as_ref() { - serializer.prompt(prompt.as_ref()); + if !self.prompt.is_empty() { + serializer.prompt(&self.prompt.as_query()); } if let Some(domain_hint) = self.domain_hint.as_ref() { @@ -435,7 +437,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { response_type, nonce: None, state: None, - prompt: None, + prompt: Default::default(), domain_hint: None, login_hint: None, code_challenge: None, @@ -456,7 +458,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { response_type, nonce: None, state: None, - prompt: None, + prompt: Default::default(), domain_hint: None, login_hint: None, code_challenge: None, @@ -525,8 +527,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// grant permissions to the app. /// - **prompt=select_account** interrupts single sign-on providing account selection experience /// listing all the accounts either in session or any remembered account or an option to choose to use a different account altogether. - pub fn with_prompt(&mut self, prompt: Prompt) -> &mut Self { - self.credential.prompt = Some(prompt); + pub fn with_prompt<I: IntoIterator<Item = Prompt>>(&mut self, prompt: I) -> &mut Self { + self.credential.prompt.extend(prompt.into_iter()); self } @@ -638,7 +640,7 @@ mod test { } #[test] - fn response_mode_set() { + fn response_type_id_token_panics_when_response_mode_query() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) @@ -647,7 +649,6 @@ mod test { .unwrap(); let query = url.query().unwrap(); - assert!(query.contains("response_mode=fragment")); assert!(query.contains("response_type=code+id_token")); } diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 7a090749..bb0016ba 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -10,13 +10,13 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder!( AuthorizationCodeAssertionCredentialBuilder, @@ -49,7 +49,6 @@ pub struct AuthorizationCodeAssertionCredential { /// you registered as credentials for your application. Read about certificate credentials /// to learn how to register your certificate and the format of the assertion. pub(crate) client_assertion: String, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -84,7 +83,6 @@ impl AuthorizationCodeAssertionCredential { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }) } @@ -218,6 +216,7 @@ impl TokenCache for AuthorizationCodeAssertionCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId); @@ -231,18 +230,18 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } - self.serializer + serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .set_scope(self.app_config.scope.clone()); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { - self.serializer.redirect_uri(redirect_uri.as_str()); + serializer.redirect_uri(redirect_uri.as_str()); } if let Some(code_verifier) = self.code_verifier.as_ref() { - self.serializer.code_verifier(code_verifier.as_ref()); + serializer.code_verifier(code_verifier.as_ref()); } if let Some(refresh_token) = self.refresh_token.as_ref() { @@ -253,11 +252,11 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { ); } - self.serializer + serializer .refresh_token(refresh_token.as_ref()) .grant_type("refresh_token"); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::RefreshToken, @@ -275,11 +274,11 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { ); } - self.serializer + serializer .authorization_code(authorization_code.as_str()) .grant_type("authorization_code"); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], vec![ OAuthParameter::AuthorizationCode, @@ -337,7 +336,6 @@ impl AuthorizationCodeAssertionCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, } @@ -356,7 +354,6 @@ impl AuthorizationCodeAssertionCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: assertion.as_ref().to_owned(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, } diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 79187a94..4bb3d7eb 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -10,7 +10,6 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, @@ -19,6 +18,7 @@ use crate::identity::{ }; #[cfg(feature = "openssl")] use crate::oauth::X509Certificate; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder!( AuthorizationCodeCertificateCredentialBuilder, @@ -49,7 +49,6 @@ pub struct AuthorizationCodeCertificateCredential { /// you registered as credentials for your application. Read about certificate credentials /// to learn how to register your certificate and the format of the assertion. pub(crate) client_assertion: String, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -84,7 +83,6 @@ impl AuthorizationCodeCertificateCredential { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }) } @@ -221,6 +219,7 @@ impl TokenCache for AuthorizationCodeCertificateCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId); @@ -234,18 +233,18 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } - self.serializer + serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .set_scope(self.app_config.scope.clone()); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { - self.serializer.redirect_uri(redirect_uri.as_str()); + serializer.redirect_uri(redirect_uri.as_str()); } if let Some(code_verifier) = self.code_verifier.as_ref() { - self.serializer.code_verifier(code_verifier.as_ref()); + serializer.code_verifier(code_verifier.as_ref()); } if let Some(refresh_token) = self.refresh_token.as_ref() { @@ -256,11 +255,11 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { ); } - self.serializer + serializer .refresh_token(refresh_token.as_ref()) .grant_type("refresh_token"); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::RefreshToken, @@ -278,11 +277,11 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { ); } - self.serializer + serializer .authorization_code(authorization_code.as_str()) .grant_type("authorization_code"); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], vec![ OAuthParameter::AuthorizationCode, @@ -342,7 +341,6 @@ impl AuthorizationCodeCertificateCredentialBuilder { code_verifier: None, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: String::new(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, }; diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index c27bef0a..0061036f 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -11,13 +11,13 @@ use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::crypto::ProofKeyCodeExchange; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder!( AuthorizationCodeCredentialBuilder, @@ -55,7 +55,6 @@ pub struct AuthorizationCodeCredential { /// Required if PKCE was used in the authorization code grant request. For more information, /// see the PKCE RFC https://datatracker.ietf.org/doc/html/rfc7636. pub(crate) code_verifier: Option<String>, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -193,7 +192,6 @@ impl AuthorizationCodeCredential { refresh_token: None, client_secret: client_secret.as_ref().to_owned(), code_verifier: None, - serializer: OAuthSerializer::new(), token_cache: Default::default(), }) } @@ -217,7 +215,6 @@ impl AuthorizationCodeCredential { refresh_token: None, client_secret: client_secret.as_ref().to_owned(), code_verifier: None, - serializer: OAuthSerializer::new(), token_cache: Default::default(), }) } @@ -259,7 +256,6 @@ impl AuthorizationCodeCredentialBuilder { refresh_token: None, client_secret: client_secret.as_ref().to_owned(), code_verifier: None, - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, } @@ -281,7 +277,6 @@ impl AuthorizationCodeCredentialBuilder { refresh_token: None, client_secret: String::new(), code_verifier: None, - serializer: OAuthSerializer::new(), token_cache, }, } @@ -298,7 +293,6 @@ impl AuthorizationCodeCredentialBuilder { refresh_token: None, client_secret: String::new(), code_verifier: None, - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, } @@ -346,6 +340,7 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); @@ -355,7 +350,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { return AF::result(OAuthParameter::ClientSecret.alias()); } - self.serializer + serializer .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) .set_scope(self.app_config.scope.clone()); @@ -363,11 +358,11 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if let Some(refresh_token) = token.refresh_token.as_ref() { - self.serializer + serializer .grant_type("refresh_token") .refresh_token(refresh_token.as_ref()); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::ClientId, @@ -389,11 +384,11 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); } - self.serializer + serializer .grant_type("refresh_token") .refresh_token(refresh_token.as_ref()); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::ClientId, @@ -411,18 +406,18 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { } if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { - self.serializer.redirect_uri(redirect_uri.as_str()); + serializer.redirect_uri(redirect_uri.as_str()); } - self.serializer + serializer .authorization_code(authorization_code.as_ref()) .grant_type("authorization_code"); if let Some(code_verifier) = self.code_verifier.as_ref() { - self.serializer.code_verifier(code_verifier.as_str()); + serializer.code_verifier(code_verifier.as_str()); } - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], vec![ OAuthParameter::ClientId, diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 735c8417..d87edf2b 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -6,7 +6,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; -use crate::auth::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, IdentityResult, AF}; @@ -48,7 +48,6 @@ pub struct ClientAssertionCredential { /// workload identity federation to learn how to setup and use assertions generated from /// other identity providers. pub(crate) client_assertion: String, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -65,7 +64,6 @@ impl ClientAssertionCredential { .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: assertion.as_ref().to_string(), - serializer: Default::default(), token_cache: Default::default(), } } @@ -139,7 +137,6 @@ impl ClientAssertionCredentialBuilder { .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), client_assertion: signed_assertion.as_ref().to_owned(), - serializer: Default::default(), token_cache: Default::default(), }, } @@ -157,7 +154,6 @@ impl ClientAssertionCredentialBuilder { app_config, client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), client_assertion: signed_assertion, - serializer: Default::default(), token_cache: Default::default(), }, } @@ -172,6 +168,7 @@ impl ClientAssertionCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.client_id().to_string(); if client_id.trim().is_empty() { return AF::result(OAuthParameter::ClientId.alias()); @@ -185,14 +182,14 @@ impl TokenCredentialExecutor for ClientAssertionCredential { self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } - self.serializer + serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .set_scope(self.app_config.scope.clone()) .grant_type("client_credentials"); - self.serializer.as_credential_map( + serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::ClientId, diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index ca92ca26..d97cf6d2 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -9,7 +9,6 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; @@ -17,6 +16,7 @@ use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; pub(crate) static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; @@ -57,7 +57,6 @@ pub struct ClientCertificateCredential { /// openssl crate. This is significantly easier than having to format the assertion from /// the certificate yourself. pub(crate) client_assertion: String, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -69,7 +68,6 @@ impl ClientCertificateCredential { .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: client_assertion.as_ref().to_owned(), - serializer: Default::default(), token_cache: Default::default(), } } @@ -149,6 +147,7 @@ impl TokenCache for ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); @@ -162,14 +161,14 @@ impl TokenCredentialExecutor for ClientCertificateCredential { self.client_assertion_type = CLIENT_ASSERTION_TYPE.to_owned(); } - self.serializer + serializer .client_id(client_id.as_str()) .client_assertion(self.client_assertion.as_str()) .client_assertion_type(self.client_assertion_type.as_str()) .grant_type("client_credentials") .set_scope(self.app_config.scope.clone()); - self.serializer.as_credential_map( + serializer.as_credential_map( vec![OAuthParameter::Scope], vec![ OAuthParameter::ClientId, @@ -211,7 +210,6 @@ impl ClientCertificateCredentialBuilder { .build(), client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: Default::default(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, } @@ -230,7 +228,6 @@ impl ClientCertificateCredentialBuilder { app_config, client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), client_assertion: Default::default(), - serializer: OAuthSerializer::new(), token_cache: Default::default(), }, }; diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 90bba47e..bc449e35 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -5,8 +5,8 @@ use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{credentials::app_config::AppConfig, Authority, AzureCloudInstance}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrlParameters { diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index e150f2ef..0b4a86da 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -9,12 +9,12 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder!( ClientSecretCredentialBuilder, @@ -44,7 +44,6 @@ pub struct ClientSecretCredential { /// specification. The Basic auth pattern of instead providing credentials in the Authorization /// header, per RFC 6749 is also supported. pub(crate) client_secret: String, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -66,7 +65,6 @@ impl ClientSecretCredential { .scope(vec!["https://graph.microsoft.com/.default"]) .build(), client_secret: client_secret.as_ref().to_owned(), - serializer: OAuthSerializer::new(), token_cache: InMemoryCacheStore::new(), } } @@ -82,7 +80,6 @@ impl ClientSecretCredential { .scope(vec!["https://graph.microsoft.com/.default"]) .build(), client_secret: client_secret.as_ref().to_owned(), - serializer: OAuthSerializer::new(), token_cache: InMemoryCacheStore::new(), } } @@ -144,6 +141,7 @@ impl TokenCache for ClientSecretCredential { #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId); @@ -153,7 +151,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { return AuthorizationFailure::result(OAuthParameter::ClientSecret); } - self.serializer + serializer .client_id(client_id.as_str()) .client_secret(self.client_secret.as_str()) .grant_type("client_credentials") @@ -161,8 +159,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { // Don't include ClientId and Client Secret in the fields for form url encode because // Client Id and Client Secret are already included as basic auth. - self.serializer - .as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) + serializer.as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) } fn client_id(&self) -> &Uuid { @@ -212,7 +209,6 @@ impl ClientSecretCredentialBuilder { credential: ClientSecretCredential { app_config, client_secret: client_secret.as_ref().to_string(), - serializer: Default::default(), token_cache: InMemoryCacheStore::new(), }, } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index a4178d13..d48ee5b7 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -19,12 +19,12 @@ use graph_error::{ IdentityResult, }; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, DeviceAuthorizationResponse, ForceTokenRefresh, PollDeviceCodeEvent, PublicClientApplication, Token, TokenCredentialExecutor, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; #[cfg(feature = "interactive-auth")] use graph_error::WebViewDeviceCodeError; @@ -67,7 +67,6 @@ pub struct DeviceCodeCredential { /// A device_code is a long string used to verify the session between the client and the authorization server. /// The client uses this parameter to request the access token from the authorization server. pub(crate) device_code: Option<String>, - serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, } @@ -81,7 +80,6 @@ impl DeviceCodeCredential { app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(), refresh_token: None, device_code: Some(device_code.as_ref().to_owned()), - serializer: Default::default(), token_cache: Default::default(), } } @@ -230,12 +228,13 @@ impl TokenCredentialExecutor for DeviceCodeCredential { } fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); } - self.serializer + serializer .client_id(client_id.as_str()) .set_scope(self.app_config.scope.clone()); @@ -247,11 +246,11 @@ impl TokenCredentialExecutor for DeviceCodeCredential { ); } - self.serializer + serializer .grant_type("refresh_token") .device_code(refresh_token.as_ref()); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![], vec![ OAuthParameter::ClientId, @@ -268,11 +267,11 @@ impl TokenCredentialExecutor for DeviceCodeCredential { ); } - self.serializer + serializer .grant_type(DEVICE_CODE_GRANT_TYPE) .device_code(device_code.as_ref()); - return self.serializer.as_credential_map( + return serializer.as_credential_map( vec![], vec![ OAuthParameter::ClientId, @@ -283,7 +282,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { ); } - self.serializer.as_credential_map( + serializer.as_credential_map( vec![], vec![OAuthParameter::ClientId, OAuthParameter::Scope], ) @@ -318,7 +317,6 @@ impl DeviceCodeCredentialBuilder { app_config: AppConfig::new(client_id.as_ref()), refresh_token: None, device_code: None, - serializer: Default::default(), token_cache: Default::default(), }, } @@ -333,7 +331,6 @@ impl DeviceCodeCredentialBuilder { app_config, refresh_token: None, device_code: Some(device_code.as_ref().to_owned()), - serializer: Default::default(), token_cache: Default::default(), }, } @@ -364,7 +361,6 @@ impl DeviceCodePollingExecutor { app_config, refresh_token: None, device_code: None, - serializer: Default::default(), token_cache: Default::default(), }, } diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 3311a5bf..af093d2b 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,5 +1,5 @@ use graph_core::crypto::secure_random_32; -use graph_error::{AuthorizationFailure, IdentityResult}; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; @@ -7,9 +7,9 @@ use std::collections::HashMap; use url::Url; use uuid::*; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder_base!(ImplicitCredentialBuilder); @@ -31,9 +31,19 @@ pub struct ImplicitCredential { /// also contain code in place of token to provide an authorization code, for use in the /// authorization code flow. This id_token+code response is sometimes called the hybrid flow. pub(crate) response_type: Vec<ResponseType>, - /// Optional - /// Specifies the method that should be used to send the resulting token back to your app. - /// Defaults to query for just an access token, but fragment if the request includes an id_token. + /// Optional (recommended) + /// + /// Specifies how the identity platform should return the requested token to your app. + /// + /// Supported values: + /// + /// - query: Default when requesting an access token. Provides the code as a query string + /// parameter on your redirect URI. The query parameter isn't supported when requesting an + /// ID token by using the implicit flow. + /// - fragment: Default when requesting an ID token by using the implicit flow. + /// Also supported if requesting only a code. + /// - form_post: Executes a POST containing the code to your redirect URI. + /// Supported when requesting a code. pub(crate) response_mode: ResponseMode, /// Optional /// A value included in the request that will also be returned in the token response. @@ -82,7 +92,7 @@ impl ImplicitCredential { ) -> IdentityResult<ImplicitCredential> { Ok(ImplicitCredential { app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(), - response_type: vec![ResponseType::Token], + response_type: vec![ResponseType::Code], response_mode: ResponseMode::Query, state: None, nonce: secure_random_32()?, @@ -114,31 +124,32 @@ impl ImplicitCredential { serializer .client_id(client_id.as_str()) .nonce(self.nonce.as_str()) - .set_scope(self.app_config.scope.clone()) - .authority(azure_cloud_instance, &self.app_config.authority); + .set_scope(self.app_config.scope.clone()); let response_types: Vec<String> = self.response_type.iter().map(|s| s.to_string()).collect(); if response_types.is_empty() { - serializer.response_type("code"); + serializer.response_type(ResponseType::Code); serializer.response_mode(self.response_mode.as_ref()); } else { let response_type = response_types.join(" ").trim().to_owned(); if response_type.is_empty() { - serializer.response_type("code"); + serializer.response_type(ResponseType::Code); } else { serializer.response_type(response_type); } - // Set response_mode if self.response_type.contains(&ResponseType::IdToken) { // id_token requires fragment or form_post. The Microsoft identity // platform recommends form_post. Unless you explicitly set - // fragment then form_post is used here. Please file an issue - // if you encounter related problems. + // fragment then form_post is used here when response type is id_token. + // Please file an issue if you encounter related problems. if self.response_mode.eq(&ResponseMode::Query) { - serializer.response_mode(ResponseMode::Fragment.as_ref()); + return Err(AF::msg_err( + "response_mode", + "ResponseType::IdToken requires ResponseMode::Fragment or ResponseMode::FormPost") + ); } else { serializer.response_mode(self.response_mode.as_ref()); } @@ -149,18 +160,7 @@ impl ImplicitCredential { // https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc if self.app_config.scope.is_empty() { - if self.response_type.contains(&ResponseType::IdToken) { - serializer.add_scope("openid"); - } else { - return AuthorizationFailure::msg_result( - "scope", - format!("{} {}", - "scope must be provided or response_type must be id_token which will add openid to scope:", - "https://learn.microsoft.com/en-us/azure/active-directory/develop/scopes-oidc" - - ) - ); - } + return Err(AF::required("scope")); } if let Some(state) = self.state.as_ref() { @@ -196,13 +196,9 @@ impl ImplicitCredential { ], )?; - if let Some(authorization_url) = serializer.get(OAuthParameter::AuthorizationUrl) { - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(query.as_str())); - Ok(url) - } else { - AuthorizationFailure::msg_result("authorization_url", "Internal Error") - } + let mut uri = azure_cloud_instance.auth_uri(&self.app_config.authority)?; + uri.set_query(Some(query.as_str())); + Ok(uri) } } @@ -367,7 +363,7 @@ mod test { } #[test] - fn set_open_id_fragment2() { + fn set_response_mode_fragment() { let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer @@ -386,11 +382,12 @@ mod test { } #[test] - fn response_type_join() { + fn response_type_id_token_token_serializes() { let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) + .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) @@ -401,11 +398,12 @@ mod test { assert!(url_result.is_ok()); let url = url_result.unwrap(); let url_str = url.as_str(); - assert!(url_str.contains("response_type=id_token+token")) + assert!(url_str.contains("response_mode=fragment")); + assert!(url_str.contains("response_type=id_token+token")); } #[test] - fn response_type_join_string() { + fn response_type_id_token_token_serializes_from_string() { let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer @@ -414,6 +412,7 @@ mod test { .into_iter() .collect(), )) + .with_response_mode(ResponseMode::FormPost) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() .with_scope(["User.Read"]) @@ -424,11 +423,13 @@ mod test { assert!(url_result.is_ok()); let url = url_result.unwrap(); let url_str = url.as_str(); + assert!(url_str.contains("response_mode=form_post")); assert!(url_str.contains("response_type=id_token+token")) } #[test] - fn response_type_into_iter() { + #[should_panic] + fn response_type_id_token_panics_with_response_mode_query() { let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); authorizer @@ -439,32 +440,11 @@ mod test { .with_nonce("678910") .build(); - let url_result = authorizer.url(); - assert!(url_result.is_ok()); - let url = url_result.unwrap(); + let url = authorizer.url().unwrap(); let url_str = url.as_str(); assert!(url_str.contains("response_type=id_token")) } - #[test] - fn response_type_into_iter2() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); - authorizer - .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) - .with_redirect_uri("http://localhost:8080/myapp") - .unwrap() - .with_scope(["User.Read"]) - .with_nonce("678910") - .build(); - - let url_result = authorizer.url(); - assert!(url_result.is_ok()); - let url = url_result.unwrap(); - let url_str = url.as_str(); - assert!(url_str.contains("response_type=id_token+token")) - } - #[test] #[should_panic] fn missing_scope_panic() { @@ -489,6 +469,7 @@ mod test { .with_client_id(Uuid::new_v4().to_string()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_response_mode(ResponseMode::Fragment) .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 273cde14..24c881fc 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -9,17 +9,18 @@ use uuid::Uuid; use graph_core::crypto::secure_random_32; use graph_error::{AuthorizationFailure, IdentityResult, AF}; -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, + AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, OpenIdCredentialBuilder, Prompt, + ResponseMode, ResponseType, }; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; #[cfg(feature = "interactive-auth")] use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationQueryResponse, OpenIdCredentialBuilder, Token}; +use crate::identity::{AuthorizationQueryResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ @@ -45,7 +46,8 @@ pub struct OpenIdAuthorizationUrlParameters { /// Required - /// Must include code for OpenID Connect sign-in. pub(crate) response_type: BTreeSet<ResponseType>, - /// Optional - + /// Optional (recommended) + /// /// Specifies how the identity platform should return the requested token to your app. /// /// Specifies the method that should be used to send the resulting authorization code back @@ -132,9 +134,9 @@ impl Debug for OpenIdAuthorizationUrlParameters { } impl OpenIdAuthorizationUrlParameters { - pub fn new<T: AsRef<str>, IU: IntoUrl, U: ToString, I: IntoIterator<Item = U>>( - client_id: T, - redirect_uri: IU, + pub fn new<U: ToString, I: IntoIterator<Item = U>>( + client_id: impl TryInto<Uuid>, + redirect_uri: impl IntoUrl, scope: I, ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { let mut scope_set = BTreeSet::new(); @@ -143,15 +145,12 @@ impl OpenIdAuthorizationUrlParameters { let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let mut response_type = BTreeSet::new(); - response_type.insert(ResponseType::IdToken); - Ok(OpenIdAuthorizationUrlParameters { - app_config: AppConfig::builder(client_id.as_ref()) + app_config: AppConfig::builder(client_id) .scope(scope_set) .redirect_uri(redirect_uri.into_url().or(redirect_uri_result)?) .build(), - response_type, + response_type: BTreeSet::from([ResponseType::IdToken]), response_mode: None, nonce: secure_random_32()?, state: None, @@ -164,12 +163,9 @@ impl OpenIdAuthorizationUrlParameters { fn new_with_app_config( app_config: AppConfig, ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { - let mut response_type = BTreeSet::new(); - response_type.insert(ResponseType::IdToken); - Ok(OpenIdAuthorizationUrlParameters { app_config, - response_type, + response_type: BTreeSet::from([ResponseType::IdToken]), response_mode: None, nonce: secure_random_32()?, state: None, @@ -180,11 +176,15 @@ impl OpenIdAuthorizationUrlParameters { } pub fn builder( - client_id: impl AsRef<str>, + client_id: impl TryInto<Uuid>, ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { OpenIdAuthorizationUrlParameterBuilder::new(client_id) } + pub fn into_credential(self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new_with_auth_code(self.app_config, authorization_code) + } + pub fn url(&self) -> IdentityResult<Url> { self.authorization_url() } @@ -209,6 +209,13 @@ impl OpenIdAuthorizationUrlParameters { &self, interactive_web_view_options: Option<WebViewOptions>, ) -> WebViewResult<AuthorizationQueryResponse> { + if self.response_mode.eq(&Some(ResponseMode::FormPost)) { + return Err(AF::msg_err( + "response_mode", + "interactive auth does not support ResponseMode::FormPost at this time", + )) + .map_err(WebViewError::from); + } let uri = self .url() .map_err(|err| Box::new(AuthExecutionError::from(err)))?; @@ -312,18 +319,17 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer .client_id(client_id.as_str()) - .nonce(self.nonce.as_str()) - .authority(azure_cloud_instance, &self.app_config.authority); + .nonce(self.nonce.as_str()); if self.response_type.is_empty() { - serializer.response_type("code"); + serializer.response_type(ResponseType::Code); } else { let response_types = self.response_type.as_query(); if !RESPONSE_TYPES_SUPPORTED.contains(&response_types.as_str()) { return AuthorizationFailure::msg_result( "response_type", format!( - "response_type is not supported - supported response types are: {}", + "provided response_type is not supported - supported response types are: {}", RESPONSE_TYPES_SUPPORTED .iter() .map(|s| format!("`{}`", s)) @@ -337,6 +343,13 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { } if let Some(response_mode) = self.response_mode.as_ref() { + if response_mode.eq(&ResponseMode::Query) { + return Err(AF::msg_err( + "response_mode", + "openid does not support ResponseMode::Query", + )); + } + serializer.response_mode(response_mode.as_ref()); } @@ -377,12 +390,9 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { ], )?; - let authorization_url = serializer - .get(OAuthParameter::AuthorizationUrl) - .ok_or(AF::msg_err("authorization_url", "Internal Error"))?; - let mut url = Url::parse(authorization_url.as_str())?; - url.set_query(Some(query.as_str())); - Ok(url) + let mut uri = azure_cloud_instance.auth_uri(&self.app_config.authority)?; + uri.set_query(Some(query.as_str())); + Ok(uri) } } @@ -430,13 +440,11 @@ pub struct OpenIdAuthorizationUrlParameterBuilder { impl OpenIdAuthorizationUrlParameterBuilder { pub(crate) fn new( - client_id: impl AsRef<str>, + client_id: impl TryInto<Uuid>, ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { Ok(OpenIdAuthorizationUrlParameterBuilder { credential: OpenIdAuthorizationUrlParameters::new_with_app_config( - AppConfig::builder(client_id.as_ref()) - .scope(vec!["openid"]) - .build(), + AppConfig::builder(client_id).scope(vec!["openid"]).build(), )?, }) } @@ -459,9 +467,8 @@ impl OpenIdAuthorizationUrlParameterBuilder { Ok(self) } - pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> &mut Self { - self.credential.app_config.client_id = - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); + pub fn with_client_id(&mut self, client_id: impl TryInto<Uuid>) -> &mut Self { + self.credential.app_config.client_id = client_id.try_into().unwrap_or_default(); self } @@ -534,22 +541,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// Takes an iterator of scopes to use in the request. /// Replaces current scopes if any were added previously. pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - if self.credential.app_config.scope.contains("offline_access") { - self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); - self.with_offline_access(); - } else { - self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); - } - self - } - - /// Adds the `offline_access` scope parameter which tells the authorization server - /// to include a refresh token in the response. - pub fn with_offline_access(&mut self) -> &mut Self { - self.credential - .app_config - .scope - .extend(vec!["offline_access".to_owned()]); + self.credential.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); self } @@ -619,9 +611,17 @@ impl OpenIdAuthorizationUrlParameterBuilder { self.credential.clone() } + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { + self.credential.url_with_host(azure_cloud_instance) + } + pub fn url(&self) -> IdentityResult<Url> { self.credential.url() } + + pub fn into_credential(self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new_with_auth_code(self.credential.app_config, authorization_code) + } } #[cfg(test)] @@ -630,8 +630,8 @@ mod test { #[test] #[should_panic] - fn code_token_unsupported_response_type() { - let _ = OpenIdAuthorizationUrlParameters::builder("client_id") + fn panics_on_invalid_response_type_code_token() { + let _ = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) .with_scope(["scope"]) @@ -641,7 +641,7 @@ mod test { #[test] #[should_panic] - fn id_token_token_unsupported_response_type() { + fn panics_on_invalid_client_id() { let _ = OpenIdAuthorizationUrlParameters::builder("client_id") .unwrap() .with_response_type([ResponseType::Token]) @@ -649,4 +649,16 @@ mod test { .url() .unwrap(); } + + #[test] + fn scope_openid_automatically_set() { + let url = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) + .unwrap() + .with_response_type([ResponseType::Code]) + .with_scope(["user.read"]) + .url() + .unwrap(); + let query = url.query().unwrap(); + assert!(query.contains("scope=openid+user.read")) + } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 4f6118c5..e12bf1bd 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -368,7 +368,6 @@ impl OpenIdCredentialBuilder { } } - #[cfg(feature = "interactive-auth")] pub(crate) fn new_with_auth_code( mut app_config: AppConfig, authorization_code: impl AsRef<str>, diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 9253d1fd..6d197703 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,6 +1,6 @@ -use crate::auth::{OAuthParameter, OAuthSerializer}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use async_trait::async_trait; use graph_error::{IdentityResult, AF}; use std::collections::HashMap; @@ -25,7 +25,6 @@ pub struct ResourceOwnerPasswordCredential { /// Required /// The user's password. pub(crate) password: String, - serializer: OAuthSerializer, } impl Debug for ResourceOwnerPasswordCredential { @@ -48,7 +47,6 @@ impl ResourceOwnerPasswordCredential { .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - serializer: Default::default(), } } @@ -64,7 +62,6 @@ impl ResourceOwnerPasswordCredential { .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - serializer: Default::default(), } } @@ -76,6 +73,7 @@ impl ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { + let mut serializer = OAuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AF::result(OAuthParameter::ClientId.alias()); @@ -89,12 +87,12 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { return AF::result(OAuthParameter::Password.alias()); } - self.serializer + serializer .client_id(client_id.as_str()) .grant_type("password") .set_scope(self.app_config.scope.clone()); - self.serializer.as_credential_map( + serializer.as_credential_map( vec![OAuthParameter::Scope], vec![OAuthParameter::ClientId, OAuthParameter::GrantType], ) @@ -127,9 +125,8 @@ impl ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder { credential: ResourceOwnerPasswordCredential { app_config: AppConfig::new(client_id.as_ref()), - username: String::new(), - password: String::new(), - serializer: Default::default(), + username: Default::default(), + password: Default::default(), }, } } @@ -144,7 +141,6 @@ impl ResourceOwnerPasswordCredentialBuilder { app_config, username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), - serializer: Default::default(), }, } } diff --git a/graph-oauth/src/jwt.rs b/graph-oauth/src/jwt.rs deleted file mode 100644 index e2603b44..00000000 --- a/graph-oauth/src/jwt.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::collections::HashMap; -use std::convert::TryFrom; -use std::str::FromStr; - -use base64::Engine; -use serde_json::Map; -use serde_json::Value; - -use graph_error::{GraphFailure, GraphResult}; - -use crate::oauth_error::OAuthError; - -/// Enum for the type of JSON web token (JWT). -#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub enum JwtType { - JWS, - JWE, -} - -impl AsRef<str> for JwtType { - fn as_ref(&self) -> &str { - match self { - JwtType::JWE => "JWE", - JwtType::JWS => "JWS", - } - } -} - -impl TryFrom<usize> for JwtType { - type Error = GraphFailure; - - fn try_from(value: usize) -> Result<Self, Self::Error> { - match value { - 2 => Ok(JwtType::JWS), - 4 => Ok(JwtType::JWE), - _ => OAuthError::invalid_data("Invalid Key"), - } - } -} - -impl FromStr for JwtType { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "payload" => Ok(JwtType::JWS), - "ciphertext" => Ok(JwtType::JWE), - _ => Err(()), - } - } -} - -/// Claims in a JSON web token (JWT). -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -pub struct Claim { - key: String, - value: Value, -} - -impl Claim { - pub fn new(key: String, value: Value) -> Claim { - Claim { key, value } - } - - pub fn key(&self) -> String { - self.key.clone() - } - - pub fn value(&self) -> Value { - self.value.clone() - } -} - -impl Eq for Claim {} - -/// Algorithms used in JSON web tokens (JWT). -/// Does not implement a complete set of Algorithms used in JWTs. -#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, EnumIter)] -pub enum Algorithm { - HS256, - HS384, - HS512, - RS256, - RS384, - RS512, - ES256, - ES384, - ES512, - PS256, - PS384, -} - -impl FromStr for Algorithm { - type Err = (); - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s { - "HS256" => Ok(Algorithm::HS256), - "HS384" => Ok(Algorithm::HS384), - "HS512" => Ok(Algorithm::HS512), - "RS256" => Ok(Algorithm::RS256), - "RS384" => Ok(Algorithm::RS384), - "RS512" => Ok(Algorithm::RS512), - "ES256" => Ok(Algorithm::ES256), - "ES384" => Ok(Algorithm::ES384), - "ES512" => Ok(Algorithm::ES512), - "PS256" => Ok(Algorithm::PS256), - "PS384" => Ok(Algorithm::PS384), - _ => Err(()), - } - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct Header { - typ: Option<String>, - alg: Algorithm, -} - -impl Header { - pub fn typ(&self) -> Option<String> { - self.typ.clone() - } - - pub fn alg(&self) -> Algorithm { - self.alg - } -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct JsonWebToken { - jwt_type: Option<JwtType>, - header: Option<Header>, - payload: Option<Vec<Claim>>, - signature: Option<String>, -} - -impl JsonWebToken { - pub fn header(&self) -> Option<Header> { - self.header.clone() - } - - pub fn claims(&self) -> Option<Vec<Claim>> { - self.payload.clone() - } - - pub fn signature(&self) -> Option<&String> { - self.signature.as_ref() - } -} - -// TODO(#4): JWT Validation - https://github.com/sreeise/graph-rs-sdk/issues/4 -/// JSON web token (JWT) verification for RFC 7619 -/// -/// The JWT implementation does not implement full JWT verification. -/// The validation here is best effort to follow section 7.2 of RFC 7519 for -/// JWT validation: <https://tools.ietf.org/html/rfc7519#section-7.2> -/// -/// Callers should not rely on this alone to verify JWTs -pub struct JwtParser; - -impl JwtParser { - pub fn parse(input: &str) -> GraphResult<JsonWebToken> { - // Step 1. - if !input.contains('.') { - return OAuthError::invalid_data("Invalid Key"); - } - - // Step 2. - let index = input - .find('.') - .ok_or_else(|| OAuthError::invalid("Invalid Key"))?; - - // Step 3. - let header = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&input[..index])?; - for byte in header.iter() { - let b = *byte; - if b == b'\n' || b == b' ' { - return OAuthError::invalid_data("Invalid Key"); - } - } - - // Step 4. - let utf8_header = std::str::from_utf8(&header)?; - - // Step 5. - let value = utf8_header.to_owned(); - let jwt_header: Header = serde_json::from_str(&value)?; - - let mut jwt = JsonWebToken { - header: Some(jwt_header), - ..Default::default() - }; - - // Step 6 - let count: usize = input.matches('.').count(); - let jwt_type = JwtType::try_from(count)?; - - jwt.jwt_type = Some(jwt_type); - - // Step 7. - match jwt_type { - JwtType::JWS => {} - JwtType::JWE => {} - } - - // Step 8. - let mut claims: Vec<Claim> = Vec::new(); - let key_vec: Vec<&str> = input.split('.').collect(); - let payload = key_vec.get(1); - - if let Some(p) = payload { - let t = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(&**p)?; - let v_utf8 = std::str::from_utf8(&t)?; - let v_owned = v_utf8.to_owned(); - - let claims_map: Map<String, Value> = serde_json::from_str(&v_owned)?; - - claims = claims_map - .iter() - .map(|(key, value)| Claim { - key: key.to_owned(), - value: value.to_owned(), - }) - .collect(); - } - - if let Some(c) = claims.iter().find(|v| v.key == "cty") { - let cty = c - .value - .as_str() - .ok_or_else(|| OAuthError::invalid("Invalid Key"))?; - if cty.eq("JWT") { - return JwtParser::parse(cty); - } - } else { - // Step 9. - } - // Step 10. - - jwt.payload = Some(claims); - Ok(jwt) - } - - #[allow(dead_code)] - fn contains_duplicates(&mut self, claims: Vec<Claim>) -> GraphResult<()> { - // https://tools.ietf.org/html/rfc7515#section-5.2 - // Step 4 this restriction includes that the same - // Header Parameter name also MUST NOT occur in distinct JSON object - // values that together comprise the JOSE Header. - let mut set = HashMap::new(); - for claim in claims.iter() { - if set.contains_key(&claim.key) { - return OAuthError::invalid_data("Duplicate claims"); - } - set.insert(&claim.key, &claim.value); - } - Ok(()) - } -} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index c04b4519..74e815c4 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -47,9 +47,7 @@ extern crate serde; #[macro_use] extern crate strum; -pub(crate) mod auth; -pub mod jwt; -mod oauth_error; +pub(crate) mod oauth_serializer; pub(crate) mod identity; @@ -57,12 +55,12 @@ pub(crate) mod identity; pub(crate) mod web; pub(crate) mod internal { - pub use crate::auth::*; + pub use crate::oauth_serializer::*; pub use graph_core::http::*; } pub mod extensions { - pub use crate::auth::*; + pub use crate::oauth_serializer::*; } pub mod oauth { diff --git a/graph-oauth/src/oauth_error.rs b/graph-oauth/src/oauth_error.rs deleted file mode 100644 index 3a502760..00000000 --- a/graph-oauth/src/oauth_error.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::error; -use std::error::Error; -use std::fmt; -use std::io::ErrorKind; - -use graph_error::GraphFailure; - -use crate::auth::OAuthParameter; - -/// Error implementation for OAuth -#[derive(Debug)] -pub enum OAuthError { - GraphFailure(GraphFailure), -} - -impl OAuthError { - pub fn error_kind(error_kind: ErrorKind, message: &str) -> GraphFailure { - let e = std::io::Error::new(error_kind, message); - GraphFailure::from(e) - } - - pub fn invalid_data<T>(msg: &str) -> std::result::Result<T, GraphFailure> { - Err(OAuthError::error_kind(ErrorKind::InvalidData, msg)) - } - - pub fn invalid(msg: &str) -> GraphFailure { - OAuthError::error_kind(ErrorKind::InvalidData, msg) - } - pub fn credential_error(c: OAuthParameter) -> GraphFailure { - GraphFailure::error_kind( - ErrorKind::NotFound, - format!("MISSING OR INVALID: {c:#?}").as_str(), - ) - } -} - -impl fmt::Display for OAuthError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - OAuthError::GraphFailure(ref err) => write!(f, "Graph Failure: {err}"), - } - } -} - -impl error::Error for OAuthError { - fn source<'a>(&'a self) -> Option<&(dyn Error + 'static)> { - match *self { - OAuthError::GraphFailure(ref err) => Some(err), - } - } -} - -impl From<GraphFailure> for OAuthError { - fn from(err: GraphFailure) -> Self { - OAuthError::GraphFailure(err) - } -} diff --git a/graph-oauth/src/auth.rs b/graph-oauth/src/oauth_serializer.rs similarity index 76% rename from graph-oauth/src/auth.rs rename to graph-oauth/src/oauth_serializer.rs index 78b21c31..71fee919 100644 --- a/graph-oauth/src/auth.rs +++ b/graph-oauth/src/oauth_serializer.rs @@ -2,15 +2,13 @@ use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::{BTreeSet, HashMap}; use std::default::Default; use std::fmt; +use std::fmt::Display; use url::form_urlencoded::Serializer; -use url::Url; -use graph_error::{AuthorizationFailure, GraphResult, IdentityResult, AF}; +use graph_error::{AuthorizationFailure, IdentityResult}; -use crate::identity::{AsQuery, Authority, AzureCloudInstance, Prompt}; -use crate::oauth::ResponseType; -use crate::oauth_error::OAuthError; +use crate::identity::{AsQuery, Prompt, ResponseType}; use crate::strum::IntoEnumIterator; /// Fields that represent common OAuth credentials. @@ -20,9 +18,6 @@ use crate::strum::IntoEnumIterator; pub enum OAuthParameter { ClientId, ClientSecret, - AuthorizationUrl, - TokenUrl, - RefreshTokenUrl, RedirectUri, AuthorizationCode, AccessToken, @@ -55,9 +50,6 @@ impl OAuthParameter { match self { OAuthParameter::ClientId => "client_id", OAuthParameter::ClientSecret => "client_secret", - OAuthParameter::AuthorizationUrl => "authorization_url", - OAuthParameter::TokenUrl => "access_token_url", - OAuthParameter::RefreshTokenUrl => "refresh_token_url", OAuthParameter::RedirectUri => "redirect_uri", OAuthParameter::AuthorizationCode => "code", OAuthParameter::AccessToken => "access_token", @@ -101,9 +93,9 @@ impl OAuthParameter { } } -impl ToString for OAuthParameter { - fn to_string(&self) -> String { - self.alias().to_string() +impl Display for OAuthParameter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.alias().to_string()) } } @@ -157,22 +149,12 @@ impl OAuthSerializer { /// # use graph_oauth::extensions::OAuthSerializer; /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); - /// oauth.insert(OAuthParameter::AuthorizationUrl, "https://example.com"); - /// assert!(oauth.contains(OAuthParameter::AuthorizationUrl)); - /// println!("{:#?}", oauth.get(OAuthParameter::AuthorizationUrl)); + /// oauth.insert(OAuthParameter::AuthorizationCode, "code"); + /// assert!(oauth.contains(OAuthParameter::AuthorizationCode)); + /// println!("{:#?}", oauth.get(OAuthParameter::AuthorizationCode)); /// ``` pub fn insert<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut OAuthSerializer { - let v = value.to_string(); - match oac { - OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl => { - Url::parse(v.as_ref()) - .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) - .unwrap(); - } - _ => {} - } - - self.parameters.insert(oac.to_string(), v); + self.parameters.insert(oac.to_string(), value.to_string()); self } @@ -185,23 +167,13 @@ impl OAuthSerializer { /// # use graph_oauth::extensions::OAuthSerializer; /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); - /// let entry = oauth.entry_with(OAuthParameter::AuthorizationUrl, "https://example.com"); - /// assert_eq!(entry, "https://example.com") + /// let entry = oauth.entry_with(OAuthParameter::AuthorizationCode, "code"); + /// assert_eq!(entry, "code") /// ``` pub fn entry_with<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { - let v = value.to_string(); - match oac { - OAuthParameter::TokenUrl | OAuthParameter::AuthorizationUrl => { - Url::parse(v.as_ref()) - .map_err(|_| AF::msg_internal_err("authorization_url | refresh_token_url")) - .unwrap(); - } - _ => {} - } - self.parameters .entry(oac.alias().to_string()) - .or_insert_with(|| v) + .or_insert_with(|| value.to_string()) } /// A view into a single entry in a map, which may either be vacant or occupied. @@ -220,7 +192,9 @@ impl OAuthSerializer { /// # use graph_oauth::extensions::OAuthSerializer; /// # use graph_oauth::extensions::OAuthParameter; /// # let mut oauth = OAuthSerializer::new(); - /// let a = oauth.get(OAuthParameter::AuthorizationUrl); + /// oauth.authorization_code("code"); + /// let code = oauth.get(OAuthParameter::AuthorizationCode); + /// assert_eq!("code", code.unwrap().as_str()); /// ``` pub fn get(&self, oac: OAuthParameter) -> Option<String> { self.parameters.get(oac.alias()).cloned() @@ -303,114 +277,6 @@ impl OAuthSerializer { self.insert(OAuthParameter::ClientSecret, value) } - /// Set the authorization URL. - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.authorization_url("https://example.com/authorize"); - /// ``` - pub fn authorization_url(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::AuthorizationUrl, value) - } - - /// Set the access token url of a request for OAuth - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.token_uri("https://example.com/token"); - /// ``` - pub fn token_uri(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::TokenUrl, value) - } - - /// Set the refresh token url of a request for OAuth - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.refresh_token_url("https://example.com/token"); - /// ``` - pub fn refresh_token_url(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::RefreshTokenUrl, value) - } - - /// Set the authorization, access token, and refresh token URL - /// for OAuth based on a tenant id. - /// - /// # Example - /// ```rust - /// # use crate::graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.tenant_id("tenant_id"); - /// ``` - pub fn tenant_id(&mut self, value: &str) -> &mut OAuthSerializer { - let token_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/token",); - let auth_url = format!("https://login.microsoftonline.com/{value}/oauth2/v2.0/authorize",); - - self.authorization_url(&auth_url).token_uri(&token_url) - } - - /// Set the authorization, access token, and refresh token URL - /// for OAuth based on a tenant id. - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.tenant_id("tenant_id"); - /// ``` - pub fn authority( - &mut self, - host: &AzureCloudInstance, - authority: &Authority, - ) -> &mut OAuthSerializer { - let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); - let auth_url = format!( - "{}/{}/oauth2/v2.0/authorize", - host.as_ref(), - authority.as_ref() - ); - - self.authorization_url(&auth_url).token_uri(&token_url) - } - - pub fn authority_admin_consent( - &mut self, - host: &AzureCloudInstance, - authority: &Authority, - ) -> &mut OAuthSerializer { - let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); - let auth_url = format!("{}/{}/adminconsent", host.as_ref(), authority.as_ref()); - - self.authorization_url(&auth_url).token_uri(&token_url) - } - - pub fn authority_device_code( - &mut self, - host: &AzureCloudInstance, - authority: &Authority, - ) -> &mut OAuthSerializer { - let token_url = format!("{}/{}/oauth2/v2.0/token", host.as_ref(), authority.as_ref()); - let auth_url = format!( - "{}/{}/oauth2/v2.0/devicecode", - host.as_ref(), - authority.as_ref() - ); - - self.authorization_url(&auth_url).token_uri(&token_url) - } - - pub fn legacy_authority(&mut self) -> &mut OAuthSerializer { - let url = "https://login.live.com/oauth20_desktop.srf".to_string(); - self.authorization_url(url.as_str()); - self.token_uri(url.as_str()) - } - /// Set the redirect url of a request /// /// # Example @@ -765,51 +631,10 @@ impl OAuthSerializer { pub fn contains_scope<T: ToString>(&self, scope: T) -> bool { self.scopes.contains(&scope.to_string()) } - - /// Remove a previously added scope. - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// - /// oauth.add_scope("scope"); - /// # assert!(oauth.contains_scope("scope")); - /// oauth.remove_scope("scope"); - /// # assert!(!oauth.contains_scope("scope")); - /// ``` - pub fn remove_scope<T: AsRef<str>>(&mut self, scope: T) { - self.scopes.remove(scope.as_ref()); - } - - /// Remove all scopes. - /// - /// # Example - /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); - /// - /// oauth.add_scope("Files.Read").add_scope("Files.ReadWrite"); - /// assert_eq!("Files.Read Files.ReadWrite", oauth.join_scopes(" ")); - /// - /// oauth.clear_scopes(); - /// assert!(oauth.get_scopes().is_empty()); - /// ``` - pub fn clear_scopes(&mut self) { - self.scopes.clear(); - } } impl OAuthSerializer { - pub fn get_or_else(&self, c: OAuthParameter) -> GraphResult<String> { - self.get(c).ok_or_else(|| OAuthError::credential_error(c)) - } - - pub fn ok_or(&self, oac: &OAuthParameter) -> IdentityResult<String> { - self.get(*oac).ok_or(AuthorizationFailure::required(oac)) - } - - pub fn try_as_tuple(&self, oac: &OAuthParameter) -> IdentityResult<(String, String)> { + fn try_as_tuple(&self, oac: &OAuthParameter) -> IdentityResult<(String, String)> { if oac.eq(&OAuthParameter::Scope) { if self.scopes.is_empty() { return Err(AuthorizationFailure::required(oac)); @@ -823,23 +648,6 @@ impl OAuthSerializer { } } - pub fn form_encode_credentials( - &mut self, - pairs: Vec<OAuthParameter>, - encoder: &mut Serializer<String>, - ) { - pairs - .iter() - .filter(|oac| (self.contains_key(oac.alias()) || oac.alias().eq("scope"))) - .for_each(|oac| { - if oac.alias().eq("scope") && !self.scopes.is_empty() { - encoder.append_pair("scope", self.join_scopes(" ").as_str()); - } else if let Some(val) = self.get(*oac) { - encoder.append_pair(oac.alias(), val.as_str()); - } - }); - } - pub fn encode_query( &mut self, optional_fields: Vec<OAuthParameter>, @@ -873,18 +681,6 @@ impl OAuthSerializer { Ok(serializer.finish()) } - pub fn params(&mut self, pairs: Vec<OAuthParameter>) -> GraphResult<HashMap<String, String>> { - let mut map: HashMap<String, String> = HashMap::new(); - for oac in pairs.iter() { - if oac.alias().eq("scope") && !self.scopes.is_empty() { - map.insert("scope".into(), self.join_scopes(" ")); - } else if let Some(val) = self.get(*oac) { - map.insert(oac.to_string(), val); - } - } - Ok(map) - } - pub fn as_credential_map( &mut self, optional_fields: Vec<OAuthParameter>, @@ -962,9 +758,6 @@ mod test { oauth .client_id("client_id") .client_secret("client_secret") - .authorization_url("https://example.com/authorize?") - .token_uri("https://example.com/token?") - .refresh_token_url("https://example.com/token?") .redirect_uri("https://example.com/redirect?") .authorization_code("ADSLFJL4L3") .response_mode("response_mode") @@ -990,18 +783,6 @@ mod test { OAuthParameter::ClientSecret => { assert_eq!(oauth.get(credential), Some("client_secret".into())) } - OAuthParameter::AuthorizationUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/authorize?".into()) - ), - OAuthParameter::TokenUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/token?".into()) - ), - OAuthParameter::RefreshTokenUrl => assert_eq!( - oauth.get(credential), - Some("https://example.com/token?".into()) - ), OAuthParameter::RedirectUri => assert_eq!( oauth.get(credential), Some("https://example.com/redirect?".into()) @@ -1057,8 +838,6 @@ mod test { .client_id("bb301aaa-1201-4259-a230923fds32") .redirect_uri("http://localhost:8888/redirect") .client_secret("CLDIE3F") - .authorization_url("https://www.example.com/authorize?") - .refresh_token_url("https://www.example.com/token?") .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); assert!(oauth.get(OAuthParameter::ClientId).is_some()); oauth.remove(OAuthParameter::ClientId); @@ -1079,9 +858,6 @@ mod test { oauth .client_id("client_id") .client_secret("client_secret") - .authorization_url("https://example.com/authorize") - .refresh_token_url("https://example.com/token") - .token_uri("https://example.com/token") .redirect_uri("https://example.com/redirect") .authorization_code("access_code"); @@ -1094,12 +870,6 @@ mod test { test_setter(OAuthParameter::ClientId, "client_id"); test_setter(OAuthParameter::ClientSecret, "client_secret"); - test_setter( - OAuthParameter::AuthorizationUrl, - "https://example.com/authorize", - ); - test_setter(OAuthParameter::RefreshTokenUrl, "https://example.com/token"); - test_setter(OAuthParameter::TokenUrl, "https://example.com/token"); test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); test_setter(OAuthParameter::AuthorizationCode, "access_code"); } diff --git a/tests/jwt_tests.rs b/tests/jwt_tests.rs deleted file mode 100644 index db2a7f78..00000000 --- a/tests/jwt_tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -use graph_oauth::jwt::{Algorithm, JwtParser}; - -// Tests that a JWT algorithm matches the one given and -// that the algorithm is not equal to any other possible matches. -fn test_jwt_validation(key: &str, alg: Algorithm) { - let jwt = JwtParser::parse(key).unwrap(); - let algorithm = jwt.header().unwrap().alg(); - assert_eq!(algorithm, alg); -} - -#[test] -fn jwt_alg() { - let key = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NTE2MTc4MDgsImV4cCI6MTU4MzE1MzgwOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiand0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoicnVzdCIsIlN1cm5hbWUiOiJvbmVkcml2ZSIsIkVtYWlsIjoiand0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJBZG1pbiIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.vgz1gffXdteqASSBz55Yl-cLmTnIv6kDxFMfe6P1BKc"; - test_jwt_validation(key, Algorithm::HS256); - - let key = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NTE2MTc4MDgsImV4cCI6MTU4MzE1MzgwOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiand0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoicnVzdCIsIlN1cm5hbWUiOiJvbmVkcml2ZSIsIkVtYWlsIjoiand0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJBZG1pbiIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.i7MTUwMJJkP8msKx_4zTnykAOT85Vyit0R0XPyHR2fFZu2UIFonFBbLNgvH-Y8Dw"; - test_jwt_validation(key, Algorithm::HS384); - - let key = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NTE2MTc4MDgsImV4cCI6MTU4MzE1MzgwOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiand0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoicnVzdCIsIlN1cm5hbWUiOiJvbmVkcml2ZSIsIkVtYWlsIjoiand0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJBZG1pbiIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.i5Vdk3PhuVleXTwhmqoBkM8NIzw6vRoTcCHml-F49sO0iQSOGechIJllxHxNe0O0U-mNw-chT8VvERY53bQJ6g"; - test_jwt_validation(key, Algorithm::HS512); - - let key = - "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TCYt5XsITJX1CxPCT8yAV-TVkIEq_PbChOMqsLfRoPsnsgw5WEuts01mq-pQy7UJiN5mgRxD-WUcX16dUEMGlv50aqzpqh4Qktb3rk-BuQy72IFLOqV0G_zS245-kronKb78cPN25DGlcTwLtjPAYuNzVBAh4vGHSrQyHUdBBPM"; - test_jwt_validation(key, Algorithm::RS256); - - let key = - "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.CN9hqUMdVb5LGo06Geb8ap1qYfbJ4rEZIMqTE9gxA2m6GGmsXkznRxzoFpAzQUey9q5HehRTk_-TxYydN3QtFPfrTbAHep7PLhp3XhdvTJ1ok__UBjv4aP6UWTF-Rflr3qeC18LdlM4nyKL7ZwSGDzytWihGod5vn4GAXErUUE4"; - test_jwt_validation(key, Algorithm::RS384); - - let key = - "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.MejLezWY6hjGgbIXkq6Qbvx_-q5vWaTR6qPiNHphvla-XaZD3up1DN6Ib5AEOVtuB3fC9l-0L36noK4qQA79lhpSK3gozXO6XPIcCp4C8MU_ACzGtYe7IwGnnK3Emr6IHQE0bpGinHX1Ak1pAuwJNawaQ6Nvmz2ozZPsyxmiwoo"; - test_jwt_validation(key, Algorithm::RS512); - - let key = - "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.P9_X1ctIxnnoUpKSWpYw3rF62e-d8LXe3sETuLn4Lhigw5OQhi-mBBKoBMneHy4kimS84zxnMby0FYo9wKM3I3pEg8Qrz0Q00tNhKCwOnZ7Q-e86sW1luK1z82tufF-sZ9_BY_LGQsym0lQmQaHFzLmEDXnOzWsjUThHGVJTI64"; - test_jwt_validation(key, Algorithm::PS256); - - let key = - "eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.oywIg-I6w59yw9jiPxewn5n2BhrD7hSifWSmzFKGBMPEMd0qweVNjlyxu2TodunPzlh49OW8QA0ygNRL9VQrWA3GXzb5FubNF4s7Y15QePx52anlvebzihx5-hR0UhKbVC0UODwYNMiY-v0L7iMbT9UvuSj0GAuZMxndo2Y2VFQ"; - test_jwt_validation(key, Algorithm::PS384); - - let key = - "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"; - test_jwt_validation(key, Algorithm::ES256); - - let key = - "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImlUcVhYSTB6YkFuSkNLRGFvYmZoa00xZi02ck1TcFRmeVpNUnBfMnRLSTgifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.cJOP_w-hBqnyTsBm3T6lOE5WpcHaAkLuQGAs1QO-lg2eWs8yyGW8p9WagGjxgvx7h9X72H7pXmXqej3GdlVbFmhuzj45A9SXDOAHZ7bJXwM1VidcPi7ZcrsMSCtP1hiN"; - test_jwt_validation(key, Algorithm::ES384); -} - -#[test] -#[should_panic] -fn invalid_jwt_hs() { - let key = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NTE2MTc4MDgsImV4cCI6MTU4MzE1MzgwOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiand0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoicnVzdCIsIlN1cm5hbWUiOiJvbmVkcml2ZSIsIkVtYWlsIjoiand0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJBZG1pbiIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.vgz1gffXdteqASSBz55Yl-cLmTnIv6kDxFMfe6P1BKc"; - test_jwt_validation(key, Algorithm::HS384); -} - -#[test] -#[should_panic] -fn invalid_jwt_rs() { - let key = - "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE1NTE2MTc4MDgsImV4cCI6MTU4MzE1MzgwOCwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiand0QGV4YW1wbGUuY29tIiwiR2l2ZW5OYW1lIjoicnVzdCIsIlN1cm5hbWUiOiJvbmVkcml2ZSIsIkVtYWlsIjoiand0QGV4YW1wbGUuY29tIiwiUm9sZSI6WyJBZG1pbiIsIlByb2plY3QgQWRtaW5pc3RyYXRvciJdfQ.vgz1gffXdteqASSBz55Yl-cLmTnIv6kDxFMfe6P1BKc"; - test_jwt_validation(key, Algorithm::RS256); -} From f987a6fbedc87996a0cc3a19802538702bea13cf Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 18 Nov 2023 09:17:28 -0500 Subject: [PATCH 064/118] Move ForceTokenRefresh to core --- Cargo.toml | 6 +- .../oauth_authorization_url/openid_connect.rs | 6 +- examples/oauth_certificate/main.rs | 48 ++++--- graph-core/Cargo.toml | 4 +- graph-core/src/cache/token_cache.rs | 3 + graph-core/src/http/response_converter.rs | 2 +- graph-core/src/identity/client_application.rs | 19 +++ graph-error/Cargo.toml | 4 +- graph-error/src/webview_error.rs | 8 +- graph-http/Cargo.toml | 4 +- graph-http/src/client.rs | 7 +- graph-oauth/Cargo.toml | 5 +- graph-oauth/src/identity/authority.rs | 121 +++++++++++++++--- .../src/identity/credentials/app_config.rs | 10 +- .../credentials/application_builder.rs | 50 +++++++- .../auth_code_authorization_url.rs | 87 +++++++++++-- ...authorization_code_assertion_credential.rs | 8 +- ...thorization_code_certificate_credential.rs | 12 +- .../authorization_code_credential.rs | 8 +- .../credentials/bearer_token_credential.rs | 11 +- .../client_assertion_credential.rs | 13 +- .../credentials/client_builder_impl.rs | 10 +- .../client_certificate_credential.rs | 11 +- .../client_credentials_authorization_url.rs | 73 +++++++++-- .../credentials/client_secret_credential.rs | 9 +- .../confidential_client_application.rs | 7 +- .../credentials/device_code_credential.rs | 13 +- .../credentials/force_token_refresh.rs | 14 -- graph-oauth/src/identity/credentials/mod.rs | 2 - .../credentials/open_id_authorization_url.rs | 9 +- .../credentials/open_id_credential.rs | 7 +- .../credentials/public_client_application.rs | 7 +- graph-oauth/src/lib.rs | 2 + src/client/graph.rs | 13 ++ 34 files changed, 461 insertions(+), 152 deletions(-) delete mode 100644 graph-oauth/src/identity/credentials/force_token_refresh.rs diff --git a/Cargo.toml b/Cargo.toml index e3a6da5b..975b9162 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,11 +30,11 @@ members = [ [dependencies] handlebars = "2.0.4" # TODO: Update to 4 -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +lazy_static = "1.4.0" +reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" -lazy_static = "1.4.0" graph-oauth = { path = "./graph-oauth", version = "1.0.2", default-features=false } graph-http = { path = "./graph-http", version = "1.1.1", default-features=false } @@ -61,7 +61,7 @@ interactive-auth = ["graph-oauth/interactive-auth"] [dev-dependencies] bytes = { version = "1.4.0" } futures = "0.3" -http = "0.2.9" +http = "0.2.11" lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } warp = { version = "0.3.5" } diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/oauth_authorization_url/openid_connect.rs index e68399d9..a601204d 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/oauth_authorization_url/openid_connect.rs @@ -50,7 +50,7 @@ fn map_to_credential( state: &str, scope: Vec<&str>, client_secret: &str, -) { +) -> IdentityResult<()> { let auth_url_builder = OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) //.with_default_scope()? @@ -59,7 +59,8 @@ fn map_to_credential( .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) .with_state(state) - .with_scope(scope); + .with_scope(scope) + .build(); // Open the url in a web browser, sign in, and get the authorization code // returned in the POST to the redirect uri. @@ -73,6 +74,7 @@ fn map_to_credential( let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); let _graph_client = GraphClient::from(&confidential_client); + Ok(()) } fn auth_url_using_confidential_client_builder( diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 23ba1b6e..51581916 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -4,9 +4,10 @@ extern crate serde; use graph_rs_sdk::oauth::{ - AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, Token, - TokenCredentialExecutor, X509Certificate, X509, + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, + X509, }; +use graph_rs_sdk::GraphClient; use std::fs::File; use std::io::Read; use warp::Filter; @@ -89,6 +90,18 @@ pub fn x509_certificate(client_id: &str, tenant_id: &str) -> anyhow::Result<X509 )) } +fn build_confidential_client( + authorization_code: &str, + x509certificate: X509Certificate, +) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { + Ok(ConfidentialClientApplication::builder(CLIENT_ID) + .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? + .with_tenant(TENANT) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI)? + .build()) +} + // When the authorization code comes in on the redirect from sign in, call the get_credential // method passing in the authorization code. // Building AuthorizationCodeCertificateCredential will create a ConfidentialClientApplication @@ -104,31 +117,16 @@ async fn handle_redirect( let authorization_code = access_code.code; let x509 = x509_certificate(CLIENT_ID, TENANT).unwrap(); - let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) - .with_authorization_code_x509_certificate(authorization_code, &x509) - .unwrap() - .with_tenant(TENANT) - .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) - .unwrap() - .build(); - - // Returns reqwest::Response - let response = confidential_client.execute_async().await.unwrap(); + let confidential_client = + build_confidential_client(authorization_code.as_str(), x509).unwrap(); + let graph_client = GraphClient::from(&confidential_client); + + let response = graph_client.users().list_user().send().await.unwrap(); + println!("{response:#?}"); - if response.status().is_success() { - let mut msal_token: Token = response.json().await.unwrap(); - msal_token.enable_pii_logging(true); - - // If all went well here we can print out the Access Token. - println!("AccessToken: {:#?}", msal_token); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - return Ok(Box::new("Error Logging In! You can close your browser.")); - } + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); // Generic login page response. Ok(Box::new( diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index 29ba8502..4866e12a 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -13,10 +13,10 @@ async-trait = "0.1.35" base64 = "0.21.0" dyn-clone = "1.0.14" Inflector = "0.11.4" -http = "0.2.9" +http = "0.2.11" parking_lot = "0.12.1" percent-encoding = "2" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/graph-core/src/cache/token_cache.rs b/graph-core/src/cache/token_cache.rs index 429004a4..f348750a 100644 --- a/graph-core/src/cache/token_cache.rs +++ b/graph-core/src/cache/token_cache.rs @@ -1,3 +1,4 @@ +use crate::identity::ForceTokenRefresh; use async_trait::async_trait; use graph_error::AuthExecutionError; @@ -24,4 +25,6 @@ pub trait TokenCache { fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError>; async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError>; + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); } diff --git a/graph-core/src/http/response_converter.rs b/graph-core/src/http/response_converter.rs index 0d2adb1e..93b110b5 100644 --- a/graph-core/src/http/response_converter.rs +++ b/graph-core/src/http/response_converter.rs @@ -67,7 +67,7 @@ impl ResponseConverterExt for reqwest::blocking::Response { let mut builder = http::Response::builder() .url(url) .json(&json) - .status(http::StatusCode::from(&status)) + .status(status) .version(version); for builder_header in builder.headers_mut().iter_mut() { diff --git a/graph-core/src/identity/client_application.rs b/graph-core/src/identity/client_application.rs index bbe015f0..506174d7 100644 --- a/graph-core/src/identity/client_application.rs +++ b/graph-core/src/identity/client_application.rs @@ -2,6 +2,21 @@ use async_trait::async_trait; use dyn_clone::DynClone; use graph_error::AuthExecutionResult; +#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum ForceTokenRefresh { + /// Always use the token cache first to when returning tokens. + /// Expired tokens will still cause an authorization request to + /// be called. + #[default] + Never, + /// ForceRefreshToken::Once will cause only the next authorization request + /// to ignore any tokens in cache and request a new token. Authorization + /// requests after this are treated as ForceRefreshToken::Never + Once, + /// Always make an authorization request regardless of any tokens in cache. + Always, +} + dyn_clone::clone_trait_object!(ClientApplication); #[async_trait] @@ -9,6 +24,8 @@ pub trait ClientApplication: DynClone + Send + Sync { fn get_token_silent(&mut self) -> AuthExecutionResult<String>; async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); } #[async_trait] @@ -20,4 +37,6 @@ impl ClientApplication for String { async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { Ok(self.clone()) } + + fn with_force_token_refresh(&mut self, _force_token_refresh: ForceTokenRefresh) {} } diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 6b3b55db..4b0d0603 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -16,8 +16,8 @@ base64 = "0.21.0" futures = "0.3" handlebars = "2.0.2" http-serde = "1" -http = "0.2.9" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +http = "0.2.11" +reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index 726c115f..8602cec8 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -1,4 +1,4 @@ -use crate::{AuthExecutionError, ErrorMessage}; +use crate::{AuthExecutionError, AuthorizationFailure, ErrorMessage}; #[derive(Debug, thiserror::Error)] pub enum WebViewError { @@ -35,7 +35,11 @@ pub enum WebViewError { AuthExecutionError(#[from] Box<AuthExecutionError>), } -impl WebViewError {} +impl From<AuthorizationFailure> for WebViewError { + fn from(value: AuthorizationFailure) -> Self { + WebViewError::AuthExecutionError(Box::new(AuthExecutionError::AuthorizationFailure(value))) + } +} #[derive(Debug, thiserror::Error)] pub enum WebViewDeviceCodeError { diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 1271ee17..ff98b21b 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -13,9 +13,9 @@ async-trait = "0.1.35" bytes = { version = "1.4.0", features = ["serde"] } futures = "0.3.28" handlebars = "2.0.4" -http = "0.2.9" +http = "0.2.11" percent-encoding = "2" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7.1" diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index a27fb2f7..d57bcc4b 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,5 +1,5 @@ use crate::blocking::BlockingClient; -use graph_core::identity::ClientApplication; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -256,6 +256,11 @@ impl Client { pub fn headers(&self) -> &HeaderMap { &self.headers } + + pub fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.client_application + .with_force_token_refresh(force_token_refresh); + } } impl Default for Client { diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 05cad53b..ee102200 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -22,9 +22,10 @@ base64 = "0.21.0" either = "1.9.0" dyn-clone = "1.0.14" hex = "0.4.3" -http = "0.2.9" +http = "0.2.11" +lazy_static = "1.4.0" openssl = { version = "0.10", optional=true } -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde-aux = "4.1.2" serde_json = "1" diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 342e22ab..4ad838ca 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -1,5 +1,25 @@ +use std::fmt::Display; use url::{ParseError, Url}; +lazy_static! { + pub static ref AZURE_PUBLIC_CLOUD_INSTANCE: Url = { + Url::parse(AzureCloudInstance::AzurePublic.as_ref()) + .expect("Unable to create Azure Public Cloud Instance Url") + }; + pub static ref AZURE_CHINA_CLOUD_INSTANCE: Url = { + Url::parse(AzureCloudInstance::AzureChina.as_ref()) + .expect("Unable to create Azure China Cloud Instance Url") + }; + pub static ref AZURE_GERMANY_CLOUD_INSTANCE: Url = { + Url::parse(AzureCloudInstance::AzureGermany.as_ref()) + .expect("Unable to create Azure Germany Cloud Instance Url") + }; + pub static ref AZURE_US_GOVERNMENT: Url = { + Url::parse(AzureCloudInstance::AzureUsGovernment.as_ref()) + .expect("Unable to create Azure Us Government Cloud Instance Url") + }; +} + /// STS instance (for instance https://login.microsoftonline.com for the Azure public cloud). /// Maps to the instance url string. #[derive( @@ -34,11 +54,25 @@ impl AsRef<str> for AzureCloudInstance { } } -impl TryFrom<AzureCloudInstance> for Url { - type Error = url::ParseError; +impl From<&AzureCloudInstance> for Url { + fn from(value: &AzureCloudInstance) -> Self { + match value { + AzureCloudInstance::AzurePublic => AZURE_PUBLIC_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureChina => AZURE_CHINA_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureGermany => AZURE_GERMANY_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureUsGovernment => AZURE_US_GOVERNMENT.clone(), + } + } +} - fn try_from(azure_cloud_instance: AzureCloudInstance) -> Result<Self, Self::Error> { - Url::parse(azure_cloud_instance.as_ref()) +impl From<AzureCloudInstance> for Url { + fn from(value: AzureCloudInstance) -> Self { + match value { + AzureCloudInstance::AzurePublic => AZURE_PUBLIC_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureChina => AZURE_CHINA_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureGermany => AZURE_GERMANY_CLOUD_INSTANCE.clone(), + AzureCloudInstance::AzureUsGovernment => AZURE_US_GOVERNMENT.clone(), + } } } @@ -100,10 +134,6 @@ impl AzureCloudInstance { /// maps to [Authority] #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] pub enum AadAuthorityAudience { - /// The sign-in audience was not specified - #[default] - None, - /// Users with a Microsoft work or school account in my organization’s Azure AD tenant (i.e. single tenant). /// Maps to https://[AzureCloudInstance]/[AadAuthorityAudience::AzureAdMyOrg(tenant_id)] /// or https://[instance]/[tenant_id] @@ -116,7 +146,8 @@ pub enum AadAuthorityAudience { AzureAdMyOrg(String), /// Users with a personal Microsoft account, or a work or school account in any organization’s Azure AD tenant - /// Maps to https://[AzureCloudInstance]/common/ or https://[instance]/[common]/ + /// Maps to https://[AzureCloudInstance]/common/ or https://[instance]/[common]/\ + #[default] AzureAdAndPersonalMicrosoftAccount, /// Users with a Microsoft work or school account in any organization’s Azure AD tenant (i.e. multi-tenant). @@ -128,6 +159,40 @@ pub enum AadAuthorityAudience { PersonalMicrosoftAccount, } +impl AadAuthorityAudience { + pub fn as_str(&self) -> &str { + match self { + AadAuthorityAudience::AzureAdMyOrg(tenant) => tenant.as_str(), + AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount => "common", + AadAuthorityAudience::AzureAdMultipleOrgs => "organizations", + AadAuthorityAudience::PersonalMicrosoftAccount => "consumers", + } + } +} + +impl AsRef<str> for AadAuthorityAudience { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for AadAuthorityAudience { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From<&str> for AadAuthorityAudience { + fn from(value: &str) -> Self { + match value.as_bytes() { + b"common" => AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount, + b"organizations" => AadAuthorityAudience::AzureAdMultipleOrgs, + b"consumers" => AadAuthorityAudience::PersonalMicrosoftAccount, + _ => AadAuthorityAudience::AzureAdMyOrg(value.to_string()), + } + } +} + /// Specifies which Microsoft accounts can be used for sign-in with a given application. /// See https://aka.ms/msal-net-application-configuration #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -172,14 +237,34 @@ impl Authority { _ => None, } } + + pub fn as_str(&self) -> &str { + match self { + Authority::AzureActiveDirectory | Authority::Common => "common", + Authority::AzureDirectoryFederatedServices => "adfs", + Authority::Organizations => "organizations", + Authority::Consumers => "consumers", + Authority::TenantId(tenant_id) => tenant_id.as_str(), + } + } +} + +impl From<&AadAuthorityAudience> for Authority { + fn from(value: &AadAuthorityAudience) -> Self { + match value { + AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount => Authority::Common, + AadAuthorityAudience::AzureAdMyOrg(tenant_id) => Authority::TenantId(tenant_id.clone()), + AadAuthorityAudience::AzureAdMultipleOrgs => Authority::Organizations, + AadAuthorityAudience::PersonalMicrosoftAccount => Authority::Consumers, + } + } } impl From<AadAuthorityAudience> for Authority { fn from(value: AadAuthorityAudience) -> Self { match value { - AadAuthorityAudience::None => Authority::Common, - AadAuthorityAudience::AzureAdMyOrg(tenant_id) => Authority::TenantId(tenant_id), AadAuthorityAudience::AzureAdAndPersonalMicrosoftAccount => Authority::Common, + AadAuthorityAudience::AzureAdMyOrg(tenant_id) => Authority::TenantId(tenant_id), AadAuthorityAudience::AzureAdMultipleOrgs => Authority::Organizations, AadAuthorityAudience::PersonalMicrosoftAccount => Authority::Consumers, } @@ -188,19 +273,13 @@ impl From<AadAuthorityAudience> for Authority { impl AsRef<str> for Authority { fn as_ref(&self) -> &str { - match self { - Authority::AzureActiveDirectory | Authority::Common => "common", - Authority::AzureDirectoryFederatedServices => "adfs", - Authority::Organizations => "organizations", - Authority::Consumers => "consumers", - Authority::TenantId(tenant_id) => tenant_id.as_str(), - } + self.as_str() } } -impl ToString for Authority { - fn to_string(&self) -> String { - String::from(self.as_ref()) +impl Display for Authority { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) } } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index d23a51ad..c363dca7 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -2,13 +2,14 @@ use base64::Engine; use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; +use graph_core::identity::ForceTokenRefresh; use graph_error::AF; use reqwest::header::HeaderMap; use url::Url; use uuid::Uuid; use crate::identity::{Authority, AzureCloudInstance}; -use crate::oauth::{ApplicationOptions, ForceTokenRefresh}; +use crate::oauth::ApplicationOptions; #[derive(Clone, Default, PartialEq)] pub struct AppConfig { @@ -164,6 +165,13 @@ impl AppConfig { pub fn log_pii(&mut self, log_pii: bool) { self.log_pii = log_pii; } + + pub fn with_authority(&mut self, authority: Authority) { + if let Authority::TenantId(tenant_id) = &authority { + self.tenant_id = Some(tenant_id.clone()); + } + self.authority = authority; + } } #[derive(Clone, Default, PartialEq)] diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 72eb01e2..f73d943b 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -2,11 +2,11 @@ use crate::identity::{ application_options::ApplicationOptions, credentials::app_config::AppConfig, AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, - ClientAssertionCredentialBuilder, ClientCredentialsAuthorizationUrlParameterBuilder, - ClientSecretCredentialBuilder, DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, - EnvironmentCredential, OpenIdAuthorizationUrlParameterBuilder, OpenIdCredentialBuilder, - PublicClientApplication, ResourceOwnerPasswordCredential, - ResourceOwnerPasswordCredentialBuilder, + AzureCloudInstance, ClientAssertionCredentialBuilder, + ClientCredentialsAuthorizationUrlParameterBuilder, ClientSecretCredentialBuilder, + DeviceCodeCredentialBuilder, DeviceCodePollingExecutor, EnvironmentCredential, + OpenIdAuthorizationUrlParameterBuilder, OpenIdCredentialBuilder, PublicClientApplication, + ResourceOwnerPasswordCredential, ResourceOwnerPasswordCredentialBuilder, }; use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; @@ -43,6 +43,19 @@ impl ConfidentialClientApplicationBuilder { self } + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.app_config.with_authority(authority.into()); + self + } + + pub fn with_azure_cloud_instance( + &mut self, + azure_cloud_instance: AzureCloudInstance, + ) -> &mut Self { + self.app_config.azure_cloud_instance = azure_cloud_instance; + self + } + /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { @@ -86,6 +99,11 @@ impl ConfidentialClientApplicationBuilder { self } + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + /// Auth Code Authorization Url Builder pub fn auth_code_url_builder(&mut self) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new_with_app_config(self.app_config.clone()) @@ -110,7 +128,7 @@ impl ConfidentialClientApplicationBuilder { pub fn with_client_x509_certificate( self, certificate: &X509Certificate, - ) -> anyhow::Result<ClientCertificateCredentialBuilder> { + ) -> IdentityResult<ClientCertificateCredentialBuilder> { ClientCertificateCredentialBuilder::new_with_certificate(certificate, self.app_config) } @@ -128,7 +146,7 @@ impl ConfidentialClientApplicationBuilder { signed_assertion: impl AsRef<str>, ) -> ClientAssertionCredentialBuilder { ClientAssertionCredentialBuilder::new_with_signed_assertion( - signed_assertion.as_ref().to_string(), + signed_assertion, self.app_config, ) } @@ -243,6 +261,19 @@ impl PublicClientApplicationBuilder { self } + pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { + self.app_config.with_authority(authority.into()); + self + } + + pub fn with_azure_cloud_instance( + &mut self, + azure_cloud_instance: AzureCloudInstance, + ) -> &mut Self { + self.app_config.azure_cloud_instance = azure_cloud_instance; + self + } + /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { @@ -286,6 +317,11 @@ impl PublicClientApplicationBuilder { self } + pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { + self.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self + } + pub fn with_device_code_executor(self) -> DeviceCodePollingExecutor { DeviceCodePollingExecutor::new_with_app_config(self.app_config) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index cc4bbadc..4857b969 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -11,16 +11,20 @@ use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; use graph_error::{IdentityResult, AF}; use crate::identity::{ - credentials::app_config::AppConfig, AsQuery, AuthorizationUrl, AzureCloudInstance, Prompt, - ResponseMode, ResponseType, + credentials::app_config::AppConfig, AsQuery, AuthorizationCodeAssertionCredentialBuilder, + AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, + ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +#[cfg(feature = "openssl")] +use crate::identity::{AuthorizationCodeCertificateCredentialBuilder, X509Certificate}; + #[cfg(feature = "interactive-auth")] use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationQueryResponse, Token}; +use crate::identity::{AuthorizationQueryResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ @@ -179,6 +183,36 @@ impl AuthCodeAuthorizationUrlParameters { self.authorization_url_with_host(azure_cloud_instance) } + pub fn into_credential( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new_with_auth_code(self.app_config, authorization_code) + } + + pub fn into_assertion_credential( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + self.app_config, + authorization_code, + ) + } + + #[cfg(feature = "openssl")] + pub fn into_certificate_credential( + self, + authorization_code: impl AsRef<str>, + x509: &X509Certificate, + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + self.app_config, + authorization_code, + x509, + ) + } + /// Get the nonce. /// /// This value may be generated automatically by the client and may be useful for users @@ -478,9 +512,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { &mut self, response_type: I, ) -> &mut Self { - self.credential - .response_type - .extend(response_type.into_iter()); + self.credential.response_type = response_type.into_iter().collect(); self } @@ -612,6 +644,39 @@ impl AuthCodeAuthorizationUrlParameterBuilder { pub fn url(&self) -> IdentityResult<Url> { self.credential.url() } + + pub fn into_credential( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new_with_auth_code( + self.credential.app_config, + authorization_code, + ) + } + + pub fn into_assertion_credential( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + self.credential.app_config, + authorization_code, + ) + } + + #[cfg(feature = "openssl")] + pub fn into_certificate_credential( + self, + authorization_code: impl AsRef<str>, + x509: &X509Certificate, + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + self.credential.app_config, + authorization_code, + x509, + ) + } } #[cfg(test)] @@ -640,16 +705,17 @@ mod test { } #[test] + #[should_panic] fn response_type_id_token_panics_when_response_mode_query() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) - .with_response_type(ResponseType::IdToken) + .with_response_mode(ResponseMode::Query) + .with_response_type(vec![ResponseType::IdToken]) .url() .unwrap(); - let query = url.query().unwrap(); - assert!(query.contains("response_type=code+id_token")); + let _query = url.query().unwrap(); } #[test] @@ -685,13 +751,10 @@ mod test { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) - .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .url() .unwrap(); let query = url.query().unwrap(); - assert!(query.contains("response_mode=fragment")); - assert!(query.contains("response_type=code+id_token")); assert!(query.contains("nonce")); } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index bb0016ba..555f35ce 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -8,13 +8,13 @@ use reqwest::IntoUrl; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, - ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, + ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -211,6 +211,10 @@ impl TokenCache for AuthorizationCodeAssertionCredential { } } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 4bb3d7eb..7a203c11 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -8,16 +8,16 @@ use reqwest::IntoUrl; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; +#[cfg(feature = "openssl")] +use crate::identity::X509Certificate; use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, - ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, + ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; -#[cfg(feature = "openssl")] -use crate::oauth::X509Certificate; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder!( @@ -214,6 +214,10 @@ impl TokenCache for AuthorizationCodeCertificateCredential { } } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 0061036f..c7473821 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -9,12 +9,12 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::crypto::ProofKeyCodeExchange; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, - TokenCredentialExecutor, + Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -175,6 +175,10 @@ impl TokenCache for AuthorizationCodeCredential { } } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } impl AuthorizationCodeCredential { diff --git a/graph-oauth/src/identity/credentials/bearer_token_credential.rs b/graph-oauth/src/identity/credentials/bearer_token_credential.rs index 08fd193f..beee98b8 100644 --- a/graph-oauth/src/identity/credentials/bearer_token_credential.rs +++ b/graph-oauth/src/identity/credentials/bearer_token_credential.rs @@ -1,7 +1,8 @@ use async_trait::async_trait; use graph_core::cache::AsBearer; -use graph_core::identity::ClientApplication; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use graph_error::AuthExecutionResult; +use std::fmt::Display; #[derive(Clone)] pub struct BearerTokenCredential(String); @@ -16,9 +17,9 @@ impl BearerTokenCredential { } } -impl ToString for BearerTokenCredential { - fn to_string(&self) -> String { - self.0.to_string() +impl Display for BearerTokenCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } @@ -55,4 +56,6 @@ impl ClientApplication for BearerTokenCredential { async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String> { Ok(self.0.clone()) } + + fn with_force_token_refresh(&mut self, _force_token_refresh: ForceTokenRefresh) {} } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index d87edf2b..de7965a8 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -8,12 +8,13 @@ use uuid::Uuid; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, Token, - TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, + Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, + CLIENT_ASSERTION_TYPE, }; credential_builder!( @@ -118,6 +119,10 @@ impl TokenCache for ClientAssertionCredential { Ok(msal_token) } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[derive(Clone)] @@ -143,7 +148,7 @@ impl ClientAssertionCredentialBuilder { } pub(crate) fn new_with_signed_assertion( - signed_assertion: String, + signed_assertion: impl AsRef<str>, mut app_config: AppConfig, ) -> ClientAssertionCredentialBuilder { app_config @@ -153,7 +158,7 @@ impl ClientAssertionCredentialBuilder { credential: ClientAssertionCredential { app_config, client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), - client_assertion: signed_assertion, + client_assertion: signed_assertion.as_ref().to_owned(), token_cache: Default::default(), }, } diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index e5418ebe..addb42fd 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -19,7 +19,7 @@ macro_rules! credential_builder_base { &mut self, authority: T, ) -> &mut Self { - self.credential.app_config.authority = authority.into(); + self.credential.app_config.with_authority(authority.into()); self } @@ -101,14 +101,6 @@ macro_rules! credential_builder { pub fn build(&self) -> $client { <$client>::new(self.credential.clone()) } - - pub fn force_token_refresh( - &mut self, - force_token_refresh: ForceTokenRefresh, - ) -> &mut Self { - self.credential.app_config.force_token_refresh = force_token_refresh; - self - } } }; } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index d97cf6d2..98e28abe 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -7,6 +7,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; @@ -14,7 +15,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::X509Certificate; use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, - ConfidentialClientApplication, ForceTokenRefresh, Token, TokenCredentialExecutor, + ConfidentialClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -142,6 +143,10 @@ impl TokenCache for ClientCertificateCredential { Ok(msal_token) } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] @@ -219,7 +224,7 @@ impl ClientCertificateCredentialBuilder { pub(crate) fn new_with_certificate( x509: &X509Certificate, mut app_config: AppConfig, - ) -> anyhow::Result<ClientCertificateCredentialBuilder> { + ) -> IdentityResult<ClientCertificateCredentialBuilder> { app_config .scope .insert("https://graph.microsoft.com/.default".into()); @@ -236,7 +241,7 @@ impl ClientCertificateCredentialBuilder { } #[cfg(feature = "openssl")] - pub fn with_certificate(&mut self, certificate: &X509Certificate) -> anyhow::Result<&mut Self> { + pub fn with_certificate(&mut self, certificate: &X509Certificate) -> IdentityResult<&mut Self> { if let Some(tenant_id) = self.credential.app_config.authority.tenant_id() { self.with_client_assertion(certificate.sign_with_tenant(Some(tenant_id.clone()))?); } else { diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index bc449e35..636aefb0 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -6,8 +6,12 @@ use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult}; use crate::identity::{credentials::app_config::AppConfig, Authority, AzureCloudInstance}; +use crate::oauth::{ClientAssertionCredentialBuilder, ClientSecretCredentialBuilder}; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +#[cfg(feature = "openssl")] +use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; + #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrlParameters { /// The client (application) ID of the service principal @@ -37,6 +41,29 @@ impl ClientCredentialsAuthorizationUrlParameters { ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } + pub fn into_credential(self, client_secret: impl AsRef<str>) -> ClientSecretCredentialBuilder { + ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) + } + + pub fn into_assertion_credential( + self, + signed_assertion: impl AsRef<str>, + ) -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder::new_with_signed_assertion( + signed_assertion, + self.app_config, + ) + } + + #[cfg(feature = "openssl")] + pub fn into_certificate_credential( + self, + _client_secret: impl AsRef<str>, + x509: &X509Certificate, + ) -> IdentityResult<ClientCertificateCredentialBuilder> { + ClientCertificateCredentialBuilder::new_with_certificate(x509, self.app_config) + } + pub fn url(&self) -> IdentityResult<Url> { self.url_with_host(&self.app_config.azure_cloud_instance) } @@ -74,13 +101,13 @@ impl ClientCredentialsAuthorizationUrlParameters { #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrlParameterBuilder { - parameters: ClientCredentialsAuthorizationUrlParameters, + credential: ClientCredentialsAuthorizationUrlParameters, } impl ClientCredentialsAuthorizationUrlParameterBuilder { pub fn new(client_id: impl AsRef<str>) -> Self { Self { - parameters: ClientCredentialsAuthorizationUrlParameters { + credential: ClientCredentialsAuthorizationUrlParameters { app_config: AppConfig::new(client_id.as_ref()), state: None, }, @@ -89,7 +116,7 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { pub(crate) fn new_with_app_config(app_config: AppConfig) -> Self { Self { - parameters: ClientCredentialsAuthorizationUrlParameters { + credential: ClientCredentialsAuthorizationUrlParameters { app_config, state: None, }, @@ -97,38 +124,64 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { } pub fn with_client_id<T: AsRef<str>>(&mut self, client_id: T) -> IdentityResult<&mut Self> { - self.parameters.app_config.client_id = Uuid::try_parse(client_id.as_ref())?; + self.credential.app_config.client_id = Uuid::try_parse(client_id.as_ref())?; Ok(self) } pub fn with_redirect_uri<T: IntoUrl>(&mut self, redirect_uri: T) -> IdentityResult<&mut Self> { let redirect_uri_result = Url::parse(redirect_uri.as_str()); let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; - self.parameters.app_config.redirect_uri = Some(redirect_uri); + self.credential.app_config.redirect_uri = Some(redirect_uri); Ok(self) } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant<T: AsRef<str>>(&mut self, tenant: T) -> &mut Self { - self.parameters.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); + self.credential.app_config.authority = Authority::TenantId(tenant.as_ref().to_owned()); self } pub fn with_authority<T: Into<Authority>>(&mut self, authority: T) -> &mut Self { - self.parameters.app_config.authority = authority.into(); + self.credential.app_config.authority = authority.into(); self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { - self.parameters.state = Some(state.as_ref().to_owned()); + self.credential.state = Some(state.as_ref().to_owned()); self } pub fn build(&self) -> ClientCredentialsAuthorizationUrlParameters { - self.parameters.clone() + self.credential.clone() } pub fn url(&self) -> IdentityResult<Url> { - self.parameters.url() + self.credential.url() + } + + pub fn into_credential(self, client_secret: impl AsRef<str>) -> ClientSecretCredentialBuilder { + ClientSecretCredentialBuilder::new_with_client_secret( + client_secret, + self.credential.app_config, + ) + } + + pub fn into_assertion_credential( + self, + signed_assertion: impl AsRef<str>, + ) -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder::new_with_signed_assertion( + signed_assertion, + self.credential.app_config, + ) + } + + #[cfg(feature = "openssl")] + pub fn into_certificate_credential( + self, + _client_secret: impl AsRef<str>, + x509: &X509Certificate, + ) -> IdentityResult<ClientCertificateCredentialBuilder> { + ClientCertificateCredentialBuilder::new_with_certificate(x509, self.credential.app_config) } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 0b4a86da..2c296fc1 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -7,12 +7,13 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, - ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, - ForceTokenRefresh, Token, TokenCredentialExecutor, + ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, + TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -136,6 +137,10 @@ impl TokenCache for ClientSecretCredential { Ok(msal_token) } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 7c8081ab..575addee 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -8,7 +8,7 @@ use url::Url; use uuid::Uuid; use graph_core::cache::{AsBearer, TokenCache}; -use graph_core::identity::ClientApplication; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::{ @@ -72,6 +72,11 @@ impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication let token = self.credential.get_token_silent_async().await?; Ok(token.as_bearer()) } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.credential + .with_force_token_refresh(force_token_refresh); + } } #[async_trait] diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index d48ee5b7..39342bcc 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -21,8 +21,8 @@ use graph_error::{ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, DeviceAuthorizationResponse, ForceTokenRefresh, - PollDeviceCodeEvent, PublicClientApplication, Token, TokenCredentialExecutor, + Authority, AzureCloudInstance, DeviceAuthorizationResponse, PollDeviceCodeEvent, + PublicClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -35,6 +35,7 @@ use crate::web::WebViewOptions; #[cfg(feature = "interactive-auth")] use crate::web::{HostOptions, InteractiveAuth, UserEvents}; +use graph_core::identity::ForceTokenRefresh; #[cfg(feature = "interactive-auth")] use wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -168,7 +169,7 @@ impl TokenCache for DeviceCodeCredential { ForceTokenRefresh::Once | ForceTokenRefresh::Always => { let token_result = self.execute_cached_token_refresh(cache_id); if self.app_config.force_token_refresh == ForceTokenRefresh::Once { - self.app_config.force_token_refresh = ForceTokenRefresh::Never; + self.with_force_token_refresh(ForceTokenRefresh::Never); } token_result } @@ -208,12 +209,16 @@ impl TokenCache for DeviceCodeCredential { ForceTokenRefresh::Once | ForceTokenRefresh::Always => { let token_result = self.execute_cached_token_refresh_async(cache_id).await; if self.app_config.force_token_refresh == ForceTokenRefresh::Once { - self.app_config.force_token_refresh = ForceTokenRefresh::Never; + self.with_force_token_refresh(ForceTokenRefresh::Never); } token_result } } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } impl TokenCredentialExecutor for DeviceCodeCredential { diff --git a/graph-oauth/src/identity/credentials/force_token_refresh.rs b/graph-oauth/src/identity/credentials/force_token_refresh.rs deleted file mode 100644 index 86584baf..00000000 --- a/graph-oauth/src/identity/credentials/force_token_refresh.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum ForceTokenRefresh { - /// Always use the token cache first to when returning tokens. - /// Expired tokens will still cause an authorization request to - /// be called. - #[default] - Never, - /// ForceRefreshToken::Once will cause only the next authorization request - /// to ignore any tokens in cache and request a new token. Authorization - /// requests after this are treated as ForceRefreshToken::Never - Once, - /// Always make an authorization request regardless of any tokens in cache. - Always, -} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 6be7d44e..9d1f50ff 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -14,7 +14,6 @@ pub use confidential_client_application::*; pub use device_code_credential::*; pub use display::*; pub use environment_credential::*; -pub use force_token_refresh::*; pub use open_id_authorization_url::*; pub use open_id_credential::*; pub use prompt::*; @@ -47,7 +46,6 @@ mod confidential_client_application; mod device_code_credential; mod display; mod environment_credential; -mod force_token_refresh; mod open_id_authorization_url; mod open_id_credential; mod prompt; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 24c881fc..c6b58146 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -17,7 +17,7 @@ use crate::identity::{ use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; #[cfg(feature = "interactive-auth")] -use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; +use graph_error::{WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] use crate::identity::{AuthorizationQueryResponse, Token}; @@ -213,12 +213,9 @@ impl OpenIdAuthorizationUrlParameters { return Err(AF::msg_err( "response_mode", "interactive auth does not support ResponseMode::FormPost at this time", - )) - .map_err(WebViewError::from); + ))?; } - let uri = self - .url() - .map_err(|err| Box::new(AuthExecutionError::from(err)))?; + let uri = self.url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index e12bf1bd..9c5137c8 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -9,11 +9,12 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; +use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, ForceTokenRefresh, + Authority, AzureCloudInstance, ConfidentialClientApplication, OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, TokenCredentialExecutor, }; @@ -220,6 +221,10 @@ impl TokenCache for OpenIdCredential { } } } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] diff --git a/graph-oauth/src/identity/credentials/public_client_application.rs b/graph-oauth/src/identity/credentials/public_client_application.rs index a7d883ce..f3111a27 100644 --- a/graph-oauth/src/identity/credentials/public_client_application.rs +++ b/graph-oauth/src/identity/credentials/public_client_application.rs @@ -6,7 +6,7 @@ use crate::identity::{ }; use async_trait::async_trait; use graph_core::cache::{AsBearer, TokenCache}; -use graph_core::identity::ClientApplication; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use graph_error::{AuthExecutionResult, IdentityResult}; use reqwest::Response; use std::collections::HashMap; @@ -56,6 +56,11 @@ impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication let token = self.credential.get_token_silent_async().await?; Ok(token.as_bearer()) } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.credential + .with_force_token_refresh(force_token_refresh); + } } #[async_trait] diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 74e815c4..8214bede 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -46,6 +46,8 @@ extern crate serde; #[macro_use] extern crate strum; +#[macro_use] +extern crate lazy_static; pub(crate) mod oauth_serializer; diff --git a/src/client/graph.rs b/src/client/graph.rs index c5507044..f92e296d 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -70,6 +70,7 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; +use graph_core::identity::ForceTokenRefresh; use graph_oauth::oauth::{DeviceCodeCredential, OpenIdCredential, PublicClientApplication}; use lazy_static::lazy_static; @@ -187,6 +188,18 @@ impl GraphClient { &self.endpoint } + pub fn with_force_token_refresh( + &mut self, + force_token_refresh: ForceTokenRefresh, + ) -> &mut Self { + self.client.with_force_token_refresh(force_token_refresh); + self + } + + pub fn set_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.client.with_force_token_refresh(force_token_refresh); + } + /// Set a custom endpoint for the Microsoft Graph API. Provide the scheme and host with an /// optional path. The path is not set by the sdk when using a custom endpoint. /// From 08caa6a7e5972bae897bc3373026b1d97ba7f8bb Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:02:44 -0500 Subject: [PATCH 065/118] Return response as error on failed silent token auth --- .github/workflows/build.yml | 2 +- .../auth_code_grant/auth_code_certificate.rs | 68 ++++++++ .../oauth_certificate/auth_code_grant/mod.rs | 2 + .../auth_code_grant/server_example/mod.rs | 152 ++++++++++++++++ .../client_credentials_certificate.rs | 57 ++++++ .../client_credentials/mod.rs | 1 + examples/oauth_certificate/main.rs | 162 +----------------- graph-error/src/authorization_failure.rs | 53 ++++-- graph-error/src/graph_failure.rs | 101 ++++++----- graph-error/src/webview_error.rs | 2 +- .../authorization_code_credential.rs | 31 +++- .../client_assertion_credential.rs | 64 +++++-- .../client_certificate_credential.rs | 64 +++++-- .../credentials/client_secret_credential.rs | 70 +++++--- graph-oauth/src/identity/credentials/mod.rs | 2 + .../credentials/token_credential_executor.rs | 21 +-- test-tools/Cargo.toml | 1 + test-tools/src/oauth_request.rs | 10 +- tests/async_concurrency.rs | 4 +- tests/download_error.rs | 6 +- tests/drive_download_request.rs | 6 +- tests/drive_request.rs | 8 +- tests/graph_error.rs | 2 +- tests/job_status.rs | 2 +- tests/mail_folder_request.rs | 6 +- tests/message_request.rs | 6 +- tests/onenote_request.rs | 6 +- tests/paging.rs | 4 +- tests/reports_request.rs | 4 +- tests/todo_tasks_request.rs | 2 +- tests/upload_request.rs | 4 +- tests/upload_session_request.rs | 2 +- tests/user_request.rs | 4 +- 33 files changed, 592 insertions(+), 337 deletions(-) create mode 100644 examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs create mode 100644 examples/oauth_certificate/auth_code_grant/mod.rs create mode 100644 examples/oauth_certificate/auth_code_grant/server_example/mod.rs create mode 100644 examples/oauth_certificate/client_credentials/client_credentials_certificate.rs create mode 100644 examples/oauth_certificate/client_credentials/mod.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2743e6d9..97683c06 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,7 @@ jobs: build: runs-on: ubuntu-latest + environment: test-environment steps: - uses: actions/checkout@v3.0.2 @@ -33,7 +34,6 @@ jobs: run: cargo build --verbose - name: Run tests env: - TEST_CREDENTIALS: ${{ secrets.TEST_CREDENTIALS }} APP_REGISTRATIONS: ${{ secrets.APP_REGISTRATIONS }} run: cargo test --verbose diff --git a/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs b/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs new file mode 100644 index 00000000..e3f9ec55 --- /dev/null +++ b/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs @@ -0,0 +1,68 @@ +use graph_rs_sdk::oauth::{ + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, + X509, +}; +use graph_rs_sdk::GraphClient; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +pub fn x509_certificate( + client_id: &str, + tenant: &str, + public_key_path: impl AsRef<Path>, + private_key_path: impl AsRef<Path>, +) -> anyhow::Result<X509Certificate> { + // Use include_bytes!(file_path) if the files are local + let mut cert_file = File::open(public_key_path)?; + let mut certificate: Vec<u8> = Vec::new(); + cert_file.read_to_end(&mut certificate)?; + + let mut private_key_file = File::open(private_key_path)?; + let mut private_key: Vec<u8> = Vec::new(); + private_key_file.read_to_end(&mut private_key)?; + + let cert = X509::from_pem(certificate.as_slice())?; + let pkey = PKey::private_key_from_pem(private_key.as_slice())?; + Ok(X509Certificate::new_with_tenant( + client_id, tenant, cert, pkey, + )) +} + +fn build_confidential_client( + authorization_code: &str, + client_id: &str, + tenant: &str, + scope: Vec<&str>, + redirect_uri: &str, + x509certificate: X509Certificate, +) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { + Ok(ConfidentialClientApplication::builder(client_id) + .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? + .with_tenant(tenant) + .with_scope(scope) + .with_redirect_uri(redirect_uri)? + .build()) +} + +fn build_graph_client( + authorization_code: &str, + client_id: &str, + tenant: &str, + scope: Vec<&str>, + redirect_uri: &str, + x509certificate: X509Certificate, +) -> anyhow::Result<()> { + let confidential_client = build_confidential_client( + authorization_code, + client_id, + tenant, + scope, + redirect_uri, + x509certificate, + )?; + + let _graph_client = GraphClient::from(&confidential_client); + + Ok(()) +} diff --git a/examples/oauth_certificate/auth_code_grant/mod.rs b/examples/oauth_certificate/auth_code_grant/mod.rs new file mode 100644 index 00000000..b7917864 --- /dev/null +++ b/examples/oauth_certificate/auth_code_grant/mod.rs @@ -0,0 +1,2 @@ +mod auth_code_certificate; +mod server_example; diff --git a/examples/oauth_certificate/auth_code_grant/server_example/mod.rs b/examples/oauth_certificate/auth_code_grant/server_example/mod.rs new file mode 100644 index 00000000..b097af06 --- /dev/null +++ b/examples/oauth_certificate/auth_code_grant/server_example/mod.rs @@ -0,0 +1,152 @@ +use graph_rs_sdk::oauth::{ + AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, + X509, +}; +use graph_rs_sdk::GraphClient; +use std::fs::File; +use std::io::Read; +use warp::Filter; + +// Requires feature openssl be enabled for graph-rs-sdk or graph-oauth + +// X509 certificates can be used for the auth code grant with +// a certificate (AuthorizationCodeCertificateCredential) and +// the client credentials grant with a certificate (ClientCertificateCredential). + +// The example below shows using the authorization code grant with a certificate. + +// This flow uses an X509 certificate for authorization. The public key should +// be uploaded to Azure Active Directory. In order to use the certificate +// flow the ClientAssertion struct can be used to generate the needed +// client assertion given an X509 certificate public key and private key. + +// If you want the client to generate a client assertion for you it +// requires the openssl feature be enabled. There are two openssl +// exports provided in this library: X509 and Pkey (private key) that will +// be used to generate the client assertion. You only need to provide these +// to the library in order to generate the client assertion. + +// You can use any way you want to get the public and private key. This example below uses +// File to get the contents of the X509 and private key, but if these files are local +// then consider using Rust's include_bytes macro which takes a local path to a file and returns the +// contents of that file as bytes. This is the expected format by X509 and Pkey in openssl. + +static CLIENT_ID: &str = "<CLIENT_ID>"; + +// Only required for certain applications. Used here as an example. +static TENANT: &str = "<TENANT_ID>"; + +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; + +static SCOPE: &str = "User.Read"; + +// The path to the public key file. +static CERTIFICATE_PATH: &str = "<CERTIFICATE_PATH>"; + +// The path to the private key file of the certificate. +static PRIVATE_KEY_PATH: &str = "<PRIVATE_KEY_PATH>"; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct AccessCode { + code: String, +} + +pub fn authorization_sign_in() { + let url = AuthorizationCodeCertificateCredential::authorization_url_builder(CLIENT_ID) + .with_tenant(TENANT) + .with_redirect_uri(REDIRECT_URI) + .with_scope(vec![SCOPE]) + .url() + .unwrap(); + + // web browser crate in dev dependencies will open to default browser in the system. + webbrowser::open(url.as_str()).unwrap(); +} + +pub fn x509_certificate() -> anyhow::Result<X509Certificate> { + // Use include_bytes!(file_path) if the files are local + let mut cert_file = File::open(PRIVATE_KEY_PATH)?; + let mut certificate: Vec<u8> = Vec::new(); + cert_file.read_to_end(&mut certificate)?; + + let mut private_key_file = File::open(CERTIFICATE_PATH)?; + let mut private_key: Vec<u8> = Vec::new(); + private_key_file.read_to_end(&mut private_key)?; + + let cert = X509::from_pem(certificate.as_slice())?; + let pkey = PKey::private_key_from_pem(private_key.as_slice())?; + Ok(X509Certificate::new_with_tenant( + CLIENT_ID, TENANT, cert, pkey, + )) +} + +fn build_confidential_client( + authorization_code: &str, + x509certificate: X509Certificate, +) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { + Ok(ConfidentialClientApplication::builder(CLIENT_ID) + .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? + .with_tenant(TENANT) + .with_scope(vec![SCOPE]) + .with_redirect_uri(REDIRECT_URI)? + .build()) +} + +// When the authorization code comes in on the redirect from sign in, call the get_credential +// method passing in the authorization code. +// Building AuthorizationCodeCertificateCredential will create a ConfidentialClientApplication +// which can be used to exchange the authorization code for an access token. +async fn handle_redirect( + code_option: Option<AccessCode>, +) -> Result<Box<dyn warp::Reply>, warp::Rejection> { + match code_option { + Some(access_code) => { + // Print out the code for debugging purposes. + println!("{:#?}", access_code.code); + + let authorization_code = access_code.code; + let x509 = x509_certificate().unwrap(); + + let confidential_client = + build_confidential_client(authorization_code.as_str(), x509).unwrap(); + let graph_client = GraphClient::from(&confidential_client); + + let response = graph_client.users().list_user().send().await.unwrap(); + + println!("{response:#?}"); + + let body: serde_json::Value = response.json().await.unwrap(); + println!("{body:#?}"); + + // Generic login page response. + Ok(Box::new( + "Successfully Logged In! You can close your browser.", + )) + } + None => Err(warp::reject()), + } +} + +/// # Example +/// ``` +/// use graph_rs_sdk::*: +/// +/// #[tokio::main] +/// async fn main() { +/// start_server_main().await; +/// } +/// ``` +pub async fn start_server_main() { + let query = warp::query::<AccessCode>() + .map(Some) + .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); + + let routes = warp::get() + .and(warp::path("redirect")) + .and(query) + .and_then(handle_redirect); + + authorization_sign_in(); + + warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; +} diff --git a/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs b/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs new file mode 100644 index 00000000..b1369494 --- /dev/null +++ b/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs @@ -0,0 +1,57 @@ +use graph_rs_sdk::oauth::{ + ClientCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, X509, +}; +use graph_rs_sdk::GraphClient; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +pub fn x509_certificate( + client_id: &str, + tenant: &str, + public_key_path: impl AsRef<Path>, + private_key_path: impl AsRef<Path>, +) -> anyhow::Result<X509Certificate> { + // Use include_bytes!(file_path) if the files are local + let mut cert_file = File::open(public_key_path)?; + let mut certificate: Vec<u8> = Vec::new(); + cert_file.read_to_end(&mut certificate)?; + + let mut private_key_file = File::open(private_key_path)?; + let mut private_key: Vec<u8> = Vec::new(); + private_key_file.read_to_end(&mut private_key)?; + + let cert = X509::from_pem(certificate.as_slice())?; + let pkey = PKey::private_key_from_pem(private_key.as_slice())?; + Ok(X509Certificate::new_with_tenant( + client_id, tenant, cert, pkey, + )) +} + +fn build_confidential_client( + client_id: &str, + tenant: &str, + scope: Vec<&str>, + x509certificate: X509Certificate, +) -> anyhow::Result<ConfidentialClientApplication<ClientCertificateCredential>> { + Ok(ConfidentialClientApplication::builder(client_id) + .with_client_x509_certificate(&x509certificate)? + .with_tenant(tenant) + .with_scope(scope) + .build()) +} + +fn build_graph_client( + authorization_code: &str, + client_id: &str, + tenant: &str, + scope: Vec<&str>, + redirect_uri: &str, + x509certificate: X509Certificate, +) -> anyhow::Result<()> { + let confidential_client = build_confidential_client(client_id, tenant, scope, x509certificate)?; + + let _graph_client = GraphClient::from(&confidential_client); + + Ok(()) +} diff --git a/examples/oauth_certificate/client_credentials/mod.rs b/examples/oauth_certificate/client_credentials/mod.rs new file mode 100644 index 00000000..67e34e06 --- /dev/null +++ b/examples/oauth_certificate/client_credentials/mod.rs @@ -0,0 +1 @@ +mod client_credentials_certificate; diff --git a/examples/oauth_certificate/main.rs b/examples/oauth_certificate/main.rs index 51581916..6dc3bcb5 100644 --- a/examples/oauth_certificate/main.rs +++ b/examples/oauth_certificate/main.rs @@ -1,162 +1,10 @@ -#![allow(dead_code)] +#![allow(dead_code, unused, unused_imports, clippy::module_inception)] + +mod auth_code_grant; +mod client_credentials; #[macro_use] extern crate serde; -use graph_rs_sdk::oauth::{ - AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, - X509, -}; -use graph_rs_sdk::GraphClient; -use std::fs::File; -use std::io::Read; -use warp::Filter; - #[tokio::main] -async fn main() { - start_server_main().await; -} - -// Requires feature openssl be enabled for graph-rs-sdk or graph-oauth - -// X509 certificates can be used for the auth code grant with -// a certificate (AuthorizationCodeCertificateCredential) and -// the client credentials grant with a certificate (ClientCertificateCredential). - -// The example below shows using the authorization code grant with a certificate. - -// This flow uses an X509 certificate for authorization. The public key should -// be uploaded to Azure Active Directory. In order to use the certificate -// flow the ClientAssertion struct can be used to generate the needed -// client assertion given an X509 certificate public key and private key. - -// If you want the client to generate a client assertion for you it -// requires the openssl feature be enabled. There are two openssl -// exports provided in this library: X509 and Pkey (private key) that will -// be used to generate the client assertion. You only need to provide these -// to the library in order to generate the client assertion. - -// You can use any way you want to get the public and private key. This example below uses -// File to get the contents of the X509 and private key, but if these files are local -// then consider using Rust's include_bytes macro which takes a local path to a file and returns the -// contents of that file as bytes. This is the expected format by X509 and Pkey in openssl. - -static CLIENT_ID: &str = "<CLIENT_ID>"; - -// Only required for certain applications. Used here as an example. -static TENANT: &str = "<TENANT_ID>"; - -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; - -static SCOPE: &str = "User.Read"; - -// The path to the public key file. -static CERTIFICATE_PATH: &str = "<CERTIFICATE_PATH>"; - -// The path to the private key file of the certificate. -static PRIVATE_KEY_PATH: &str = "<PRIVATE_KEY_PATH>"; - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct AccessCode { - code: String, -} - -pub fn authorization_sign_in() { - let url = AuthorizationCodeCertificateCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT) - .with_redirect_uri(REDIRECT_URI) - .with_scope(vec![SCOPE]) - .url() - .unwrap(); - - // web browser crate in dev dependencies will open to default browser in the system. - webbrowser::open(url.as_str()).unwrap(); -} - -pub fn x509_certificate(client_id: &str, tenant_id: &str) -> anyhow::Result<X509Certificate> { - // Use include_bytes!(file_path) if the files are local - let mut cert_file = File::open(PRIVATE_KEY_PATH)?; - let mut certificate: Vec<u8> = Vec::new(); - cert_file.read_to_end(&mut certificate)?; - - let mut private_key_file = File::open(CERTIFICATE_PATH)?; - let mut private_key: Vec<u8> = Vec::new(); - private_key_file.read_to_end(&mut private_key)?; - - let cert = X509::from_pem(certificate.as_slice())?; - let pkey = PKey::private_key_from_pem(private_key.as_slice())?; - Ok(X509Certificate::new_with_tenant( - client_id, tenant_id, cert, pkey, - )) -} - -fn build_confidential_client( - authorization_code: &str, - x509certificate: X509Certificate, -) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { - Ok(ConfidentialClientApplication::builder(CLIENT_ID) - .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? - .with_tenant(TENANT) - .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI)? - .build()) -} - -// When the authorization code comes in on the redirect from sign in, call the get_credential -// method passing in the authorization code. -// Building AuthorizationCodeCertificateCredential will create a ConfidentialClientApplication -// which can be used to exchange the authorization code for an access token. -async fn handle_redirect( - code_option: Option<AccessCode>, -) -> Result<Box<dyn warp::Reply>, warp::Rejection> { - match code_option { - Some(access_code) => { - // Print out the code for debugging purposes. - println!("{:#?}", access_code.code); - - let authorization_code = access_code.code; - let x509 = x509_certificate(CLIENT_ID, TENANT).unwrap(); - - let confidential_client = - build_confidential_client(authorization_code.as_str(), x509).unwrap(); - let graph_client = GraphClient::from(&confidential_client); - - let response = graph_client.users().list_user().send().await.unwrap(); - - println!("{response:#?}"); - - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); - - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) - } - None => Err(warp::reject()), - } -} - -/// # Example -/// ``` -/// use graph_rs_sdk::*: -/// -/// #[tokio::main] -/// async fn main() { -/// start_server_main().await; -/// } -/// ``` -pub async fn start_server_main() { - let query = warp::query::<AccessCode>() - .map(Some) - .or_else(|_| async { Ok::<(Option<AccessCode>,), std::convert::Infallible>((None,)) }); - - let routes = warp::get() - .and(warp::path("redirect")) - .and(query) - .and_then(handle_redirect); - - authorization_sign_in(); - - warp::serve(routes).run(([127, 0, 0, 1], 8080)).await; -} +async fn main() {} diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 31e56a3f..462cd411 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,4 +1,4 @@ -use crate::IdentityResult; +use crate::{ErrorMessage, IdentityResult}; use tokio::sync::mpsc::error::SendTimeoutError; pub type AF = AuthorizationFailure; @@ -14,19 +14,19 @@ pub enum AuthorizationFailure { }, #[error("{0:#?}")] - UrlParseError(#[from] url::ParseError), + UrlParse(#[from] url::ParseError), #[error("{0:#?}")] - UuidError(#[from] uuid::Error), + Uuid(#[from] uuid::Error), #[error("{0:#?}")] Unknown(String), #[error("{0:#?}")] - X509Error(String), + OpenSsl(String), #[error("{0:#?}")] - SerdeJsonError(#[from] serde_json::Error), + SerdeJson(#[from] serde_json::Error), } impl AuthorizationFailure { @@ -35,7 +35,7 @@ impl AuthorizationFailure { } pub fn unknown_result<T: ToString>(value: T) -> IdentityResult<AuthorizationFailure> { - Err(AuthorizationFailure::Unknown(value.to_string())) + Err(AuthorizationFailure::unknown(value)) } pub fn required<T: AsRef<str>>(name: T) -> AuthorizationFailure { @@ -80,10 +80,6 @@ impl AuthorizationFailure { Err(AF::msg_internal_err(name)) } - pub fn url_parse_error<T>(url_parse_error: url::ParseError) -> Result<T, AuthorizationFailure> { - Err(AuthorizationFailure::UrlParseError(url_parse_error)) - } - pub fn condition(cond: bool, name: &str, msg: &str) -> IdentityResult<()> { if cond { AF::msg_result(name, msg) @@ -93,11 +89,11 @@ impl AuthorizationFailure { } pub fn x509(message: impl ToString) -> AuthorizationFailure { - AuthorizationFailure::X509Error(message.to_string()) + AuthorizationFailure::OpenSsl(message.to_string()) } pub fn x509_result<T>(message: impl ToString) -> Result<T, AuthorizationFailure> { - Err(AuthorizationFailure::X509Error(message.to_string())) + Err(AuthorizationFailure::OpenSsl(message.to_string())) } } @@ -107,13 +103,36 @@ impl AuthorizationFailure { #[derive(Debug, thiserror::Error)] pub enum AuthExecutionError { #[error("{0:#?}")] - AuthorizationFailure(#[from] AuthorizationFailure), - #[error("{0:#?}")] - RequestError(#[from] reqwest::Error), + Authorization(#[from] AuthorizationFailure), + #[error("{0:#?}")] - SerdeError(#[from] serde_json::error::Error), + Request(#[from] reqwest::Error), + #[error("{0:#?}")] - HttpError(#[from] http::Error), + Http(#[from] http::Error), + + #[error("message: {0:#?}, response: {1:#?}", message, response)] + SilentTokenAuth { + message: String, + response: http::Response<Result<serde_json::Value, ErrorMessage>>, + }, +} + +impl AuthExecutionError { + pub fn silent_token_auth( + response: http::Response<Result<serde_json::Value, ErrorMessage>>, + ) -> AuthExecutionError { + AuthExecutionError::SilentTokenAuth { + message: "silent token auth failed".into(), + response, + } + } +} + +impl From<serde_json::error::Error> for AuthExecutionError { + fn from(value: serde_json::error::Error) -> Self { + AuthExecutionError::Authorization(AuthorizationFailure::from(value)) + } } #[derive(Debug, thiserror::Error)] diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index f79d5bfc..6e5c722c 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -11,46 +11,46 @@ use std::sync::mpsc; #[derive(Debug, thiserror::Error)] #[allow(clippy::large_enum_variant)] pub enum GraphFailure { - #[error("IO error:\n{0:#?}")] + #[error("{0:#?}")] Io(#[from] io::Error), - #[error("Base 64 decode error:\n{0:#?}")] + #[error("{0:#?}")] Utf8Error(#[from] Utf8Error), - #[error("Request error:\n{0:#?}")] + #[error("{0:#?}")] ReqwestError(#[from] reqwest::Error), - #[error("Serde error:\n{0:#?}")] - SerdeError(#[from] serde_json::error::Error), + #[error("{0:#?}")] + SerdeJson(#[from] serde_json::Error), - #[error("Base64 decode error:\n{0:#?}")] + #[error("{0:#?}")] DecodeError(#[from] base64::DecodeError), - #[error("Recv error:\n{0:#?}")] + #[error("{0:#?}")] RecvError(#[from] mpsc::RecvError), - #[error("Borrow Mut Error error:\n{0:#?}")] + #[error("{0:#?}")] BorrowMutError(#[from] BorrowMutError), - #[error("Url parse error:\n{0:#?}")] - UrlParseError(#[from] url::ParseError), + #[error("{0:#?}")] + UrlParse(#[from] url::ParseError), - #[error("http::Error:\n{0:#?}")] + #[error("{0:#?}")] HttpError(#[from] http::Error), - #[error("Internal error:\n{0:#?}")] + #[error("{0:#?}")] GraphRsError(#[from] GraphRsError), - #[error("Handlebars render error:\n{0:#?}")] + #[error("{0:#?}")] HandlebarsRenderError(#[from] handlebars::RenderError), - #[error("Handlebars template render error:\n{0:?}")] + #[error("{0:?}")] HandlebarsTemplateRenderError(#[from] handlebars::TemplateRenderError), #[error("Crypto Error (Unknown)")] CryptoError, - #[error("Async Download Error:\n{0:#?}")] + #[error("{0:#?}")] AsyncDownloadError(#[from] AsyncDownloadError), #[error( @@ -73,6 +73,12 @@ pub enum GraphFailure { #[error("{0:#?}")] ErrorMessage(#[from] ErrorMessage), + + #[error("message: {0:#?}, response: {1:#?}", message, response)] + SilentTokenAuth { + message: String, + response: http::Response<Result<serde_json::Value, ErrorMessage>>, + }, } impl GraphFailure { @@ -113,43 +119,46 @@ impl From<ring::error::Unspecified> for GraphFailure { impl From<AuthExecutionError> for GraphFailure { fn from(value: AuthExecutionError) -> Self { match value { - AuthExecutionError::AuthorizationFailure(authorization_failure) => { - match authorization_failure { - AuthorizationFailure::RequiredValue { name, message } => { - GraphFailure::PreFlightError { - url: None, - headers: None, - error: None, - message: format!("name: {:#?}, message: {:#?}", name, message), - } - } - AuthorizationFailure::UrlParseError(e) => GraphFailure::UrlParseError(e), - AuthorizationFailure::UuidError(_uuid_error) => GraphFailure::PreFlightError { - url: None, - headers: None, - error: None, - message: "Client Id is not a valid UUID".to_owned(), - }, - AuthorizationFailure::Unknown(message) => GraphFailure::PreFlightError { - url: None, - headers: None, - error: None, - message, - }, - AuthorizationFailure::X509Error(message) => GraphFailure::PreFlightError { + AuthExecutionError::Authorization(authorization_failure) => match authorization_failure + { + AuthorizationFailure::RequiredValue { name, message } => { + GraphFailure::PreFlightError { url: None, headers: None, error: None, - message, - }, - AuthorizationFailure::SerdeJsonError(serde_json_error) => { - GraphFailure::SerdeError(serde_json_error) + message: format!("name: {:#?}, message: {:#?}", name, message), } } + AuthorizationFailure::UrlParse(error) => GraphFailure::UrlParse(error), + AuthorizationFailure::Uuid(error) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message: format!( + "name: client_id, message: {:#?}, source: {:#?}", + "Client Id is not a valid Uuid", + error.to_string() + ), + }, + AuthorizationFailure::Unknown(message) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message, + }, + AuthorizationFailure::OpenSsl(message) => GraphFailure::PreFlightError { + url: None, + headers: None, + error: None, + message, + }, + AuthorizationFailure::SerdeJson(error) => GraphFailure::SerdeJson(error), + }, + AuthExecutionError::Request(e) => GraphFailure::ReqwestError(e), + AuthExecutionError::Http(e) => GraphFailure::HttpError(e), + AuthExecutionError::SilentTokenAuth { message, response } => { + GraphFailure::SilentTokenAuth { message, response } } - AuthExecutionError::RequestError(e) => GraphFailure::ReqwestError(e), - AuthExecutionError::SerdeError(e) => GraphFailure::SerdeError(e), - AuthExecutionError::HttpError(e) => GraphFailure::HttpError(e), } } } diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index 8602cec8..70a4490e 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -37,7 +37,7 @@ pub enum WebViewError { impl From<AuthorizationFailure> for WebViewError { fn from(value: AuthorizationFailure) -> Self { - WebViewError::AuthExecutionError(Box::new(AuthExecutionError::AuthorizationFailure(value))) + WebViewError::AuthExecutionError(Box::new(AuthExecutionError::Authorization(value))) } } diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index c7473821..8271e8e9 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -9,12 +9,14 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::crypto::ProofKeyCodeExchange; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, + EXECUTOR_TRACING_TARGET, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -69,6 +71,13 @@ impl Debug for AuthorizationCodeCredential { impl AuthorizationCodeCredential { fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + let new_token: Token = response.json()?; self.token_cache.store(cache_id, new_token.clone()); @@ -84,13 +93,19 @@ impl AuthorizationCodeCredential { cache_id: String, ) -> AuthExecutionResult<Token> { let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); if new_token.refresh_token.is_some() { self.refresh_token = new_token.refresh_token.clone(); } - - self.token_cache.store(cache_id, new_token.clone()); Ok(new_token) } } @@ -99,6 +114,7 @@ impl AuthorizationCodeCredential { impl TokenCache for AuthorizationCodeCredential { type Token = Token; + #[tracing::instrument] fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); @@ -107,6 +123,7 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { return Ok(token); } @@ -114,19 +131,23 @@ impl TokenCache for AuthorizationCodeCredential { if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); if let Some(refresh_token) = token.refresh_token.as_ref() { self.refresh_token = Some(refresh_token.to_owned()); } self.execute_cached_token_refresh(cache_id) } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token) } } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } ForceTokenRefresh::Once | ForceTokenRefresh::Always => { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); let token_result = self.execute_cached_token_refresh(cache_id); if self.app_config.force_token_refresh == ForceTokenRefresh::Once { self.app_config.force_token_refresh = ForceTokenRefresh::Never; @@ -136,6 +157,7 @@ impl TokenCache for AuthorizationCodeCredential { } } + #[tracing::instrument] async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); @@ -144,6 +166,7 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); if let Ok(token) = self .execute_cached_token_refresh_async(cache_id.clone()) .await @@ -157,12 +180,14 @@ impl TokenCache for AuthorizationCodeCredential { if let Some(refresh_token) = old_token.refresh_token.as_ref() { self.refresh_token = Some(refresh_token.to_owned()); } - + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); self.execute_cached_token_refresh_async(cache_id).await } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(old_token.clone()) } } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } } diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index de7965a8..4fdae1f5 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -8,13 +8,14 @@ use uuid::Uuid; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{AuthExecutionError, IdentityResult, AF}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, + CLIENT_ASSERTION_TYPE, EXECUTOR_TRACING_TARGET, }; credential_builder!( @@ -68,6 +69,37 @@ impl ClientAssertionCredential { token_cache: Default::default(), } } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } impl Debug for ClientAssertionCredential { @@ -82,41 +114,37 @@ impl Debug for ClientAssertionCredential { impl TokenCache for ClientAssertionCredential { type Token = Token; + #[tracing::instrument] fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token) } } else { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } } + #[tracing::instrument] async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token.clone()) } } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await } } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 98e28abe..db9f7a03 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -7,15 +7,16 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; +use graph_error::{AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; use crate::identity::{ Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, - ConfidentialClientApplication, Token, TokenCredentialExecutor, + ConfidentialClientApplication, Token, TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -92,6 +93,37 @@ impl ClientCertificateCredential { ) -> ClientCredentialsAuthorizationUrlParameterBuilder { ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } impl Debug for ClientCertificateCredential { @@ -106,41 +138,37 @@ impl Debug for ClientCertificateCredential { impl TokenCache for ClientCertificateCredential { type Token = Token; + #[tracing::instrument] fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token) } } else { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } } + #[tracing::instrument] async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token refresh"); + self.execute_cached_token_refresh_async(cache_id).await } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token.clone()) } } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request"); + self.execute_cached_token_refresh_async(cache_id).await } } diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 2c296fc1..e76ec939 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -7,13 +7,14 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{AuthExecutionError, AuthorizationFailure, IdentityResult}; +use graph_error::{AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult}; use crate::identity::{ credentials::app_config::AppConfig, Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, - TokenCredentialExecutor, + TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -90,6 +91,37 @@ impl ClientSecretCredential { ) -> ClientCredentialsAuthorizationUrlParameterBuilder { ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } } #[async_trait] @@ -100,41 +132,31 @@ impl TokenCache for ClientSecretCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token) } } else { - let response = self.execute()?; - let msal_token: Token = response.json()?; - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) } } - #[tracing::instrument] async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - tracing::debug!("tokenResponse={:#?}", &msal_token); - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!("tokenResponse={:#?}", &token); + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); Ok(token.clone()) } } else { - let response = self.execute_async().await?; - let msal_token: Token = response.json().await?; - tracing::debug!("tokenResponse={:#?}", &msal_token); - self.token_cache.store(cache_id, msal_token.clone()); - Ok(msal_token) + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await } } @@ -224,10 +246,6 @@ impl ClientSecretCredentialBuilder { self } - pub fn build_client(&self) -> ConfidentialClientApplication<ClientSecretCredential> { - ConfidentialClientApplication::credential(self.credential.clone()) - } - pub fn credential(&self) -> ClientSecretCredential { self.credential.clone() } diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 9d1f50ff..34bfe30e 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -57,3 +57,5 @@ mod token_credential_executor; #[cfg(feature = "openssl")] mod x509_certificate; + +pub(crate) const EXECUTOR_TRACING_TARGET: &str = "graph_rs_sdk::credential_executor"; diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 1324fde1..d097368a 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -11,8 +11,9 @@ use uuid::Uuid; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; -use crate::identity::AuthorizationRequestParts; -use crate::identity::{Authority, AzureCloudInstance}; +use crate::identity::{ + Authority, AuthorizationRequestParts, AzureCloudInstance, EXECUTOR_TRACING_TARGET, +}; dyn_clone::clone_trait_object!(TokenCredentialExecutor); @@ -38,7 +39,6 @@ pub trait TokenCredentialExecutor: DynClone + Debug { Ok(auth_request) } - #[tracing::instrument] fn build_request(&mut self) -> AuthExecutionResult<reqwest::blocking::RequestBuilder> { let http_client = reqwest::blocking::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -56,7 +56,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: "graph_rs_sdk::token_credential_executor", + target: EXECUTOR_TRACING_TARGET, "authorization request constructed" ); Ok(request_builder) @@ -67,14 +67,13 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: "graph_rs_sdk::token_credential_executor", + target: EXECUTOR_TRACING_TARGET, "authorization request constructed" ); Ok(request_builder) } } - #[tracing::instrument] fn build_request_async(&mut self) -> AuthExecutionResult<reqwest::RequestBuilder> { let http_client = reqwest::ClientBuilder::new() .min_tls_version(Version::TLS_1_2) @@ -92,7 +91,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: "graph_rs_sdk::token_credential_executor", + target: EXECUTOR_TRACING_TARGET, "authorization request constructed" ); Ok(request_builder) @@ -103,7 +102,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: "graph_rs_sdk::token_credential_executor", + target: EXECUTOR_TRACING_TARGET, "authorization request constructed" ); Ok(request_builder) @@ -136,21 +135,19 @@ pub trait TokenCredentialExecutor: DynClone + Debug { &self.app_config().extra_query_parameters } - #[tracing::instrument] fn execute(&mut self) -> AuthExecutionResult<reqwest::blocking::Response> { let request_builder = self.build_request()?; let response = request_builder.send()?; let status = response.status(); - tracing::debug!(target: "graph_rs_sdk::token_credential_executor", "authorization response received; status={status:#?}"); + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "authorization response received; status={status:#?}"); Ok(response) } - #[tracing::instrument] async fn execute_async(&mut self) -> AuthExecutionResult<reqwest::Response> { let request_builder = self.build_request_async()?; let response = request_builder.send().await?; let status = response.status(); - tracing::debug!(target: "graph_rs_sdk::token_credential_executor", "authorization response received; status={status:#?}"); + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "authorization response received; status={status:#?}"); Ok(response) } } diff --git a/test-tools/Cargo.toml b/test-tools/Cargo.toml index f2b6025d..a354c360 100644 --- a/test-tools/Cargo.toml +++ b/test-tools/Cargo.toml @@ -13,6 +13,7 @@ anyhow = { version = "1.0.69", features = ["backtrace"]} futures = "0.3" from_as = "0.2.0" lazy_static = "1.4.0" +parking_lot = "0.12.1" rand = "0.8" serde = {version = "1", features = ["derive"] } serde_json = "1" diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 0ddeb6a6..b69188f9 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -13,15 +13,15 @@ use std::env; use std::io::{Read, Write}; use graph_core::identity::ClientApplication; -use std::sync::Mutex; // static mutex's that are used for preventing test failures // due to too many concurrent requests (throttling) for Microsoft Graph. lazy_static! { - pub static ref THROTTLE_MUTEX: Mutex<()> = Mutex::new(()); - pub static ref DRIVE_THROTTLE_MUTEX: Mutex<()> = Mutex::new(()); - pub static ref ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); - pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); + pub static ref ASYNC_THROTTLE_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); + pub static ref ASYNC_THROTTLE_MUTEX2: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); + pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); + pub static ref DRIVE_ASYNC_THROTTLE_MUTEX2: parking_lot::Mutex<()> = + parking_lot::Mutex::new(()); } //pub const APPLICATIONS_CLIENT: Mutex<Option<(String, Graph)>> = Mutex::new(OAuthTestClient::graph_by_rid(ResourceIdentity::Applications)); diff --git a/tests/async_concurrency.rs b/tests/async_concurrency.rs index 267eed05..16315c21 100644 --- a/tests/async_concurrency.rs +++ b/tests/async_concurrency.rs @@ -3,7 +3,7 @@ use graph_http::traits::ODataNextLink; use graph_rs_sdk::*; use serde::Deserialize; use serde::Serialize; -use test_tools::oauth_request::{OAuthTestClient, ASYNC_THROTTLE_MUTEX}; +use test_tools::oauth_request::{OAuthTestClient, ASYNC_THROTTLE_MUTEX2}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserResponse { @@ -34,7 +34,7 @@ pub struct LicenseDetail { #[tokio::test] async fn buffered_requests() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX2.lock(); if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut stream = client .users() diff --git a/tests/download_error.rs b/tests/download_error.rs index ef94c365..198c8a06 100644 --- a/tests/download_error.rs +++ b/tests/download_error.rs @@ -3,13 +3,13 @@ use graph_rs_sdk::http::FileConfig; use graph_http::traits::ResponseExt; use std::ffi::OsStr; -use test_tools::oauth_request::DRIVE_ASYNC_THROTTLE_MUTEX; +use test_tools::oauth_request::DRIVE_ASYNC_THROTTLE_MUTEX2; use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn download_config_file_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .user(id.as_str()) @@ -46,7 +46,7 @@ async fn download_config_file_exists() { #[tokio::test] async fn download_is_err_config_dir_no_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .user(id.as_str()) diff --git a/tests/drive_download_request.rs b/tests/drive_download_request.rs index 29147de9..85984631 100644 --- a/tests/drive_download_request.rs +++ b/tests/drive_download_request.rs @@ -2,12 +2,12 @@ use graph_http::traits::ResponseExt; use graph_rs_sdk::http::FileConfig; use graph_rs_sdk::*; use std::ffi::OsStr; -use test_tools::oauth_request::{Environment, OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX}; +use test_tools::oauth_request::{Environment, OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX2}; use test_tools::support::cleanup::AsyncCleanUp; #[tokio::test] async fn drive_download() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .drive(id.as_str()) @@ -35,7 +35,7 @@ async fn drive_download() { #[tokio::test] async fn drive_download_format() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); if Environment::is_local() { if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client diff --git a/tests/drive_request.rs b/tests/drive_request.rs index 15533267..9b52bf56 100644 --- a/tests/drive_request.rs +++ b/tests/drive_request.rs @@ -14,7 +14,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_versions_get_item() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let get_item_res = client .user(id.as_str()) @@ -48,7 +48,7 @@ async fn list_versions_get_item() { #[tokio::test] async fn drive_check_in_out() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if Environment::is_local() { if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client @@ -95,7 +95,7 @@ async fn update_item_by_path( #[tokio::test] async fn drive_update() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let req = update_item_by_path( id.as_str(), @@ -197,7 +197,7 @@ async fn delete_file( #[tokio::test] async fn drive_upload_item() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file.txt"; let file_name = ":/test_upload_file.txt:"; diff --git a/tests/graph_error.rs b/tests/graph_error.rs index 22ee8a9d..81ece60d 100644 --- a/tests/graph_error.rs +++ b/tests/graph_error.rs @@ -6,7 +6,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn drive_download_graph_error() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .drive(id.as_str()) diff --git a/tests/job_status.rs b/tests/job_status.rs index 7ca6067c..76720272 100644 --- a/tests/job_status.rs +++ b/tests/job_status.rs @@ -22,7 +22,7 @@ async fn delete_item( #[tokio::test] async fn job_status() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); let original_file = ":/test_job_status.txt:"; let copy_name = "test_job_status_copy.txt"; diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index 815af57f..3c138e26 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -1,12 +1,12 @@ use graph_core::resource::ResourceIdentity; use graph_http::api_impl::ODataQuery; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; +use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX2; use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn get_drafts_mail_folder() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await { @@ -29,7 +29,7 @@ async fn get_drafts_mail_folder() { #[tokio::test] async fn mail_folder_list_messages() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await { diff --git a/tests/message_request.rs b/tests/message_request.rs index 0021fa0e..f8897a44 100644 --- a/tests/message_request.rs +++ b/tests/message_request.rs @@ -1,12 +1,12 @@ use std::thread; use std::time::Duration; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; +use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX2; use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_and_get_messages() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { if let Ok(response) = client .user(id.as_str()) @@ -41,7 +41,7 @@ async fn list_and_get_messages() { #[tokio::test] async fn mail_create_and_delete_message() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX2.lock(); if let Some((id, mut client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client .v1() diff --git a/tests/onenote_request.rs b/tests/onenote_request.rs index 962190b7..e77e4add 100644 --- a/tests/onenote_request.rs +++ b/tests/onenote_request.rs @@ -15,7 +15,7 @@ async fn list_get_notebooks_and_sections() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { let notebooks = client @@ -84,7 +84,7 @@ async fn create_delete_page_from_file() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { let res = client @@ -124,7 +124,7 @@ async fn download_page() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((user_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { diff --git a/tests/paging.rs b/tests/paging.rs index 4f616af4..a95d5f9c 100644 --- a/tests/paging.rs +++ b/tests/paging.rs @@ -9,7 +9,7 @@ async fn paging_all() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut vec = client .users() @@ -33,7 +33,7 @@ async fn paging_all() { #[tokio::test] async fn paging_stream() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = ASYNC_THROTTLE_MUTEX.lock(); if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut stream = client .users() diff --git a/tests/reports_request.rs b/tests/reports_request.rs index d086704f..6d29c4c0 100644 --- a/tests/reports_request.rs +++ b/tests/reports_request.rs @@ -11,7 +11,7 @@ async fn async_download_office_365_user_counts_reports_test() { return; } - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX.lock(); if let Some((_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Reports).await @@ -47,7 +47,7 @@ async fn async_download_office_365_user_counts_reports_test() { #[tokio::test] async fn get_office_365_user_counts_reports_text() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX.lock(); if let Some((_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Reports).await diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs index c3888280..338ea51b 100644 --- a/tests/todo_tasks_request.rs +++ b/tests/todo_tasks_request.rs @@ -19,7 +19,7 @@ struct TodoListsTasks { #[tokio::test] async fn list_users() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::TodoListsTasks).await { diff --git a/tests/upload_request.rs b/tests/upload_request.rs index c50c7fe3..a4dc72f8 100644 --- a/tests/upload_request.rs +++ b/tests/upload_request.rs @@ -86,7 +86,7 @@ async fn get_file_content( #[tokio::test] async fn upload_bytes_mut() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; @@ -134,7 +134,7 @@ async fn upload_bytes_mut() { #[tokio::test] async fn upload_reqwest_body() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index f1744249..e3368d15 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -162,7 +162,7 @@ async fn file_upload_session_channel( // This is a long running test. 20 - 30 seconds. #[tokio::test] async fn test_upload_session() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let item_by_path = ":/upload_session_file.txt:"; let local_file = "./test_files/upload_session_file.txt"; diff --git a/tests/user_request.rs b/tests/user_request.rs index e6c37873..ce2e3c3d 100644 --- a/tests/user_request.rs +++ b/tests/user_request.rs @@ -5,7 +5,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_users() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX.lock(); if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client.users().list_user().send().await; @@ -27,7 +27,7 @@ async fn list_users() { #[tokio::test] async fn get_user() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; + let _ = ASYNC_THROTTLE_MUTEX.lock(); if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client.users().id(id).get_user().send().await; From 40d8230bf08df1a950965543dad32df5c914f9cb Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 22 Nov 2023 20:36:10 -0500 Subject: [PATCH 066/118] Update examples and README --- README.md | 270 ++++++++++++------ .../interactive_authentication/auth_code.rs | 37 +-- examples/interactive_authentication/openid.rs | 19 +- .../webview_errors.rs | 7 +- .../webview_options.rs | 25 +- examples/oauth/README.md | 108 +++++-- .../auth_code_grant/auth_code_grant_pkce.rs | 36 +-- .../auth_code_grant/auth_code_grant_secret.rs | 7 +- .../client_credentials_secret.rs | 13 +- examples/oauth/device_code.rs | 7 +- examples/oauth/environment_credential.rs | 11 +- .../oauth/openid/server_examples/openid.rs | 27 +- graph-core/src/crypto/mod.rs | 8 +- graph-core/src/crypto/pkce.rs | 17 +- graph-error/src/authorization_failure.rs | 17 +- graph-error/src/graph_failure.rs | 8 +- graph-error/src/webview_error.rs | 2 +- graph-oauth/README.md | 201 ++++++------- .../identity/authorization_query_response.rs | 57 +++- .../src/identity/authorization_response.rs | 12 - .../auth_code_authorization_url.rs | 105 +++++-- ...authorization_code_assertion_credential.rs | 22 ++ ...thorization_code_certificate_credential.rs | 27 ++ .../credentials/legacy/implicit_credential.rs | 63 ++-- .../credentials/open_id_authorization_url.rs | 55 ++-- .../resource_owner_password_credential.rs | 92 +++++- graph-oauth/src/identity/id_token.rs | 20 -- graph-oauth/src/identity/mod.rs | 3 - graph-oauth/src/identity/token.rs | 6 +- .../src/web/interactive_authenticator.rs | 11 +- src/client/graph.rs | 33 ++- 31 files changed, 834 insertions(+), 492 deletions(-) delete mode 100644 graph-oauth/src/identity/authorization_response.rs diff --git a/README.md b/README.md index 4f3afc03..be921569 100644 --- a/README.md +++ b/README.md @@ -44,11 +44,9 @@ use graph_rs_sdk::*; ## Table Of Contents +Graph Client + * [Usage](#usage) - * [OAuth - Getting Access Tokens](#oauth---getting-access-tokens) - * [Identity Platform Support](#identity-platform-support) - * [Automatic Token Refresh](#automatic-token-refresh) - * [Interactive Authentication](#interactive-authentication) * [Async and Blocking Client](#async-and-blocking-client) * [Async Client](#async-client-default) * [Blocking Client](#blocking-client) @@ -62,6 +60,22 @@ use graph_rs_sdk::*; * [Wiki](#wiki) * [Feature Requests for Bug Reports](#feature-requests-or-bug-reports) +OAuth and Openid + +* [OAuth - Getting Access Tokens](#oauth---getting-access-tokens) + * [Identity Platform Support](#identity-platform-support) + * [Credentials](#credentials) + * [Auth Code Grant](#authorization-code-grant) + * [Authorization Code Grant Secret](#authorization-code-secret) + * [Authorization Code With Proof Key Code Exchange](#authorization-code-secret-with-proof-key-code-exchange) + * [Client Credentials](#client-credentials) + * [Client Secret Credential](#client-secret-credential) + * [Environment Credentials](#environment-credentials) + * [Client Secret Environment Credential](#client-secret-environment-credential) + * [Resource Owner Password Credential](#resource-owner-password-credential) + * [Automatic Token Refresh](#automatic-token-refresh) + * [Interactive Authentication](#interactive-authentication) + ### What APIs are available The APIs available are generated from OpenApi configs that are stored in Microsoft's msgraph-metadata repository @@ -945,7 +959,7 @@ async fn get_user() -> GraphResult<()> { ### Warning -The crate is undergoing major development in order to support all or most scenarios in the +This crate is undergoing major development in order to support all or most scenarios in the Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some unstable features. Any version that is not a pre-release version of the crate is considered stable. @@ -975,10 +989,10 @@ For more extensive examples see the directory on [GitHub](https://github.com/sreeise/graph-rs-sdk). -```rust,ignore -let confidental_client: ConfidentialClientApplication<ClientSecretCredential> = ... - -let graph_client = Graph::from(confidential_client); +```rust +fn build_client(confidential_client: ConfidentialClientApplication<ClientSecretCredential>) { + let graph_client = GraphClient::from(&confidential_client); +} ``` ### Identity Platform Support @@ -996,7 +1010,8 @@ The following flows from the Microsoft Identity Platform are supported: You can use the url builders for those flows that require an authorization code using a redirect after sign in you can use -### Examples +## Credentials + ### Authorization Code Grant @@ -1007,39 +1022,78 @@ on redirect after sign in is performed by the user. Once you have the authorization code you can pass this to the client and the client will perform the request to get an access token on the first graph api call that you make. + +#### Authorization Code Secret + ```rust use graph_rs_sdk::{ - Graph, - oauth::ConfidentialClientApplication, + Graph, + oauth::ConfidentialClientApplication, }; -#[tokio::main] -async fn main() { - let authorization_code = "<AUTH_CODE>"; - let client_id = "<CLIENT_ID>"; - let client_secret = "<CLIENT_SECRET>"; - let scope = vec!["<SCOPE>", "<SCOPE>"]; - let redirect_uri = "http://localhost:8080"; - - let mut confidential_client = ConfidentialClientApplication::builder(client_id) - .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential - .with_client_secret(client_secret) - .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() - .build(); - - let graph_client = Graph::from(confidential_client); - - let _response = graph_client - .users() - .list_user() - .send() // Also makes first access token request at this point - .await; +async fn build_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str> +) -> anyhow::Result<GraphClient> { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .unwrap() + .build(); + + let graph_client = Graph::from(confidential_client); + + Ok(graph_client) +} +``` + +#### Authorization Code Secret With Proof Key Code Exchange + +```rust +use graph_rs_sdk::oauth::{ + AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, + ProofKeyCodeExchange, TokenCredentialExecutor, +}; +use lazy_static::lazy_static; +use url::Url; + +// You can also pass your own values for PKCE instead of automatic generation by +// calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) +lazy_static! { + static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap(); +} + +fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> anyhow::Result<Url> { + Ok(AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_pkce(&PKCE) + .url()?) +} + +fn build_confidential_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<String>, +) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { + Ok(ConfidentialClientApplication::builder(client_id) + .with_auth_code(authorization_code) + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri)? + .with_pkce(&PKCE) + .build()) } ``` -### Client Credentials Grant. +### Client Credentials The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, instead of impersonating a user, to authenticate when calling another web service. The grant specified in RFC 6749, @@ -1051,6 +1105,9 @@ Client credentials flow requires a one time administrator acceptance of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) + +#### Client Secret Credential + ```rust use graph_rs_sdk::{ oauth::ConfidentialClientApplication, Graph @@ -1067,8 +1124,44 @@ pub async fn get_graph_client(tenant: &str, client_id: &str, client_secret: &str ``` +### Environment Credentials + +#### Client Secret Environment Credential + +Environment Variables: + +- AZURE_TENANT_ID (Optional/Recommended - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_CLIENT_SECRET (Required) + +```rust +pub fn client_secret_credential() -> anyhow::Result<GraphClient> { + let confidential_client = EnvironmentCredential::client_secret_credential()?; + Ok(GraphClient::from(&confidential_client)) +} +``` + +#### Resource Owner Password Credential + +Environment Variables: + +- AZURE_TENANT_ID (Optional - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_USERNAME (Required) +- AZURE_PASSWORD (Required) + +```rust +pub fn username_password() -> anyhow::Result<GraphClient> { + let public_client = EnvironmentCredential::resource_owner_password_credential()?; + Ok(GraphClient::from(&public_client)) +} +``` + + ### Automatic Token Refresh +The client stores tokens using an in memory cache. + Using automatic token refresh requires getting a refresh token as part of the token response. To get a refresh token you must include the `offline_access` scope. @@ -1079,16 +1172,42 @@ If you are using the `client credentials` grant you do not need the `offline_acc Tokens will still be automatically refreshed as this flow does not require using a refresh token to get a new access token. +The example below uses the auth code grant. + +First create the url where the user will sign in. After sign in the user will be redirected back to your app and +the authentication code will be in the query of the uri. ```rust -async fn authenticate(client_id: &str, tenant: &str, redirect_uri: &str) { +pub fn authorization_sign_in_url(client_id: &str, tenant: &str, redirect_uri: &str) -> Url { let scope = vec!["offline_access"]; - let mut credential_builder = ConfidentialClientApplication::builder(client_id) - .auth_code_url_builder() - .with_tenant(tenant) - .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + + AuthorizationCodeCredential::authorization_url_builder(client_id) .with_redirect_uri(redirect_uri) - .url(); - // ... add any other parameters you need + .with_scope(scope) + .url() + .unwrap() +} +``` + +Once you have the authorization code you can build a confidential client and pass it to the graph client. + +```rust +async fn build_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + scope: Vec<String>, // with offline_access + redirect_uri: &str, +) -> anyhow::Result<GraphClient> { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri)? + .build(); + + let graph_client = GraphClient::from(&confidential_client); + + Ok(graph_client) } ``` @@ -1106,46 +1225,31 @@ Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) cra platforms that support it such as on a desktop. ```rust -use graph_rs_sdk::oauth::{ - web::Theme, web::WebViewOptions, AuthorizationCodeCredential, - ConfidentialClientApplication -}; -use graph_rs_sdk::Graph; - -fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { - let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) - .auth_code_url_builder() - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .interactive_authentication(None) - .unwrap(); - - confidential_client_builder.with_client_secret(CLIENT_SECRET).build() -} - -async fn authenticate() { - // Create a tracing subscriber to log debug/trace events coming from - // authorization http calls and the Graph client. - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - .with_max_level(tracing::Level::TRACE) - .init(); - - let mut confidential_client = run_interactive_auth(); - - let client = Graph::from(&confidential_client); - - let response = client.user(USER_ID) - .get_user() - .send() - .await - .unwrap(); - - println!("{response:#?}"); - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); +use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; + +async fn authenticate( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str>, +) -> anyhow::Result<GraphClient> { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + + let (authorization_query_response, mut credential_builder) = + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .with_interactive_authentication_for_secret(Default::default()) + .unwrap(); + + debug!("{authorization_query_response:#?}"); + + let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + + Ok(GraphClient::from(&confidential_client)) } ``` diff --git a/examples/interactive_authentication/auth_code.rs b/examples/interactive_authentication/auth_code.rs index a34abb87..91f569b2 100644 --- a/examples/interactive_authentication/auth_code.rs +++ b/examples/interactive_authentication/auth_code.rs @@ -1,14 +1,5 @@ use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; -static CLIENT_ID: &str = "CLIENT_ID"; -static CLIENT_SECRET: &str = "CLIENT_SECRET"; -static TENANT_ID: &str = "TENANT_ID"; - -// This should be the user id for the user you are logging in as. -static USER_ID: &str = "USER_ID"; - -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; - // Requires feature=interactive_authentication // Interactive Authentication WebView Using Wry library https://github.com/tauri-apps/wry @@ -28,27 +19,27 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // and subsequent calls will use this token. If a refresh token is included, which you can get // by requesting the offline_access scope, then the confidential client will take care of refreshing // the token. -async fn authenticate() { +async fn authenticate( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str>, +) -> anyhow::Result<GraphClient> { std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); let (authorization_query_response, mut credential_builder) = - AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .with_interactive_authentication(None) + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .with_interactive_authentication_for_secret(Default::default()) .unwrap(); debug!("{authorization_query_response:#?}"); - let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); - - let client = GraphClient::from(&confidential_client); - - let response = client.user(USER_ID).get_user().send().await.unwrap(); + let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); - debug!("{response:#?}"); - let body: serde_json::Value = response.json().await.unwrap(); - debug!("{body:#?}"); + Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/interactive_authentication/openid.rs b/examples/interactive_authentication/openid.rs index 0e0240d0..0c0224c4 100644 --- a/examples/interactive_authentication/openid.rs +++ b/examples/interactive_authentication/openid.rs @@ -3,7 +3,12 @@ use graph_rs_sdk::{ GraphClient, }; -fn openid_authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) { +async fn openid_authenticate( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, +) -> anyhow::Result<()> { std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); @@ -13,10 +18,8 @@ fn openid_authenticate(tenant_id: &str, client_id: &str, client_secret: &str, re .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_redirect_uri(redirect_uri) - .unwrap() - .with_interactive_authentication(None) - .unwrap(); + .with_redirect_uri(redirect_uri)? + .with_interactive_authentication(Default::default())?; debug!("{authorization_query_response:#?}"); @@ -24,9 +27,11 @@ fn openid_authenticate(tenant_id: &str, client_id: &str, client_secret: &str, re let client = GraphClient::from(&confidential_client); - let response = client.users().list_user().into_blocking().send().unwrap(); + let response = client.users().list_user().send().await?; debug!("{response:#?}"); - let body: serde_json::Value = response.json().unwrap(); + let body: serde_json::Value = response.json().await?; debug!("{body:#?}"); + + Ok(()) } diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs index 9b6539d6..6808d3cb 100644 --- a/examples/interactive_authentication/webview_errors.rs +++ b/examples/interactive_authentication/webview_errors.rs @@ -6,7 +6,7 @@ async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, re .with_tenant(tenant_id) .with_scope(scope) .with_redirect_uri(redirect_uri) - .with_interactive_authentication(None); + .with_interactive_authentication_for_secret(None); if let Ok((authorization_query_response, credential_builder)) = credential_builder_result { // ... @@ -15,6 +15,11 @@ async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, re // Webview Window closed for one of the following reasons: // 1. The user closed the webview window without logging in. // 2. The webview exited because of a timeout defined in the WebViewOptions. + // + // Values will be one of: + // 1. CloseRequested: User closed the window before completing sign in and redirect. + // 2. TimedOut: The timeout specified in WebViewOptions was reached. By default there + // is no timeout. WebViewError::WindowClosed(reason) => {} // One of the following errors has occurred: diff --git a/examples/interactive_authentication/webview_options.rs b/examples/interactive_authentication/webview_options.rs index 726926b5..49c1eef7 100644 --- a/examples/interactive_authentication/webview_options.rs +++ b/examples/interactive_authentication/webview_options.rs @@ -1,4 +1,5 @@ use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use graph_rs_sdk::GraphClient; use std::collections::HashSet; use std::ops::Add; use std::time::{Duration, Instant}; @@ -25,11 +26,21 @@ fn get_webview_options() -> WebViewOptions { .with_ports(HashSet::from([8000])) } -async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(scope) - .with_redirect_uri(redirect_uri) - .with_interactive_authentication(Some(get_webview_options())) - .unwrap(); +async fn customize_webview( + tenant_id: &str, + client_id: &str, + client_secret: &str, + scope: Vec<&str>, + redirect_uri: &str, +) -> anyhow::Result<GraphClient> { + let (authorization_response, mut credential_builder) = + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_interactive_authentication_for_secret(get_webview_options())?; + + let confidential_client = credential_builder.with_client_secret(client_secret).build(); + + Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/oauth/README.md b/examples/oauth/README.md index 70a02e1d..b0d53f6a 100644 --- a/examples/oauth/README.md +++ b/examples/oauth/README.md @@ -1,4 +1,4 @@ -# OAuth Overview +# Identity Overview There are two main types for building your chosen OAuth or OpenId Connect Flow. @@ -6,6 +6,18 @@ There are two main types for building your chosen OAuth or OpenId Connect Flow. - `ConfidentialClientApplication` +## Table Of Contents + +* [Credentials](#credentials) + * [Authorization Code Grant](#authorization-code-grant) + * [Client Credentials](#client-credentials) + * [Client Secret Credential](#client-secret-credential) + * [Environment Credentials](#environment-credentials) + * [Client Secret Environment Credential](#client-secret-environment-credential) + * [Resource Owner Password Credential](#resource-owner-password-credential) + +## Credentials + ### Authorization Code Grant The authorization code grant is considered a confidential client (except in the hybrid flow) @@ -14,32 +26,86 @@ on redirect after sign in is performed by the user. ```rust use graph_rs_sdk::{ - Graph, - oauth::ConfidentialClientApplication, + Graph, + oauth::ConfidentialClientApplication, }; -#[tokio::main] -async fn main() { - let authorization_code = "<AUTH_CODE>"; - let client_id = "<CLIENT_ID>"; - let client_secret = "<CLIENT_SECRET>"; - let scope = vec!["<SCOPE>", "<SCOPE>"]; - let redirect_uri = "http://localhost:8080"; +async fn build_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str> +) -> anyhow::Result<GraphClient> { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential + .with_client_secret(client_secret) + .with_scope(scope) + .with_redirect_uri(redirect_uri)? + .build(); + + let graph_client = Graph::from(confidential_client); + + Ok(graph_client) +} +``` + +## Client Credentials - let mut confidential_client = ConfidentialClientApplication::builder(client_id) - .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential +The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, +instead of impersonating a user, to authenticate when calling another web service. The grant specified in RFC 6749, +sometimes called two-legged OAuth, can be used to access web-hosted resources by using the identity of an application. +This type is commonly used for server-to-server interactions that must run in the background, without immediate +interaction with a user, and is often referred to as daemons or service accounts. + +Client credentials flow requires a one time administrator acceptance +of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions +as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) + +### Client Secret Credential + +```rust +use graph_rs_sdk::{oauth::ConfidentialClientApplication, GraphClient}; + +pub async fn build_client(client_id: &str, client_secret: &str, tenant: &str) -> GraphClient { + let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) .with_client_secret(client_secret) - .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() + .with_tenant(tenant) .build(); - let graph_client = GraphClient::from(confidential_client); + GraphClient::from(&confidential_client_application) +} +``` + +### Environment Credentials + +#### Client Secret Environment Credential + +Environment Variables: - let _response = graph_client - .users() - .list_user() - .send() - .await; +- AZURE_TENANT_ID (Optional/Recommended - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_CLIENT_SECRET (Required) + +```rust +pub fn client_secret_credential() -> anyhow::Result<GraphClient> { + let confidential_client = EnvironmentCredential::client_secret_credential()?; + Ok(GraphClient::from(&confidential_client)) +} +``` + +#### Resource Owner Password Credential + +Environment Variables: + +- AZURE_TENANT_ID (Optional - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_USERNAME (Required) +- AZURE_PASSWORD (Required) + +```rust +pub fn username_password() -> anyhow::Result<GraphClient> { + let public_client = EnvironmentCredential::resource_owner_password_credential()?; + Ok(GraphClient::from(&public_client)) } ``` diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs index ad3fb77e..b4f114db 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,15 +1,11 @@ -use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::oauth::{ - AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, - GenPkce, ProofKeyCodeExchange, ResponseType, Token, TokenCredentialExecutor, + AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, ProofKeyCodeExchange, + TokenCredentialExecutor, }; use lazy_static::lazy_static; use url::Url; use warp::{get, Filter}; -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; - // You can also pass your own values for PKCE instead of automatic generation by // calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method) lazy_static! { @@ -27,13 +23,18 @@ lazy_static! { /// to in order to sign in. Then wait for the redirect after sign in to the redirect url /// you specified in your app. To see a server example listening for the redirect see /// [Auth Code Grant PKCE Server Example](https://github.com/sreeise/graph-rs-sdk/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs) -fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> Url { - AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_scope(scope) - .with_redirect_uri(redirect_uri) - .with_pkce(&PKCE) - .url() - .unwrap() +fn authorization_sign_in_url( + client_id: &str, + redirect_uri: &str, + scope: Vec<String>, +) -> anyhow::Result<Url> { + Ok( + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_pkce(&PKCE) + .url()?, + ) } fn build_confidential_client( @@ -42,13 +43,12 @@ fn build_confidential_client( client_secret: &str, redirect_uri: &str, scope: Vec<String>, -) -> ConfidentialClientApplication<AuthorizationCodeCredential> { - ConfidentialClientApplication::builder(client_id) +) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { + Ok(ConfidentialClientApplication::builder(client_id) .with_auth_code(authorization_code) .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() + .with_redirect_uri(redirect_uri)? .with_pkce(&PKCE) - .build() + .build()) } diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs index 382ea582..743c7dc3 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/oauth/auth_code_grant/auth_code_grant_secret.rs @@ -24,16 +24,15 @@ async fn auth_code_grant_secret( client_secret: &str, scope: Vec<String>, redirect_uri: &str, -) { +) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() + .with_redirect_uri(redirect_uri)? .build(); let graph_client = GraphClient::from(&confidential_client); - let _response = graph_client.users().list_user().send().await; + Ok(graph_client) } diff --git a/examples/oauth/client_credentials/client_credentials_secret.rs b/examples/oauth/client_credentials/client_credentials_secret.rs index abaf80ae..8fc5092c 100644 --- a/examples/oauth/client_credentials/client_credentials_secret.rs +++ b/examples/oauth/client_credentials/client_credentials_secret.rs @@ -5,15 +5,10 @@ use graph_rs_sdk::{oauth::ConfidentialClientApplication, GraphClient}; -// Replace client id, client secret, and tenant id with your own values. -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static TENANT_ID: &str = "<TENANT_ID>"; - -pub async fn get_graph_client() -> GraphClient { - let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_tenant(TENANT_ID) +pub async fn build_client(client_id: &str, client_secret: &str, tenant: &str) -> GraphClient { + let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) + .with_client_secret(client_secret) + .with_tenant(tenant) .build(); GraphClient::from(&confidential_client_application) diff --git a/examples/oauth/device_code.rs b/examples/oauth/device_code.rs index b2a2e6e8..25c77ab9 100644 --- a/examples/oauth/device_code.rs +++ b/examples/oauth/device_code.rs @@ -19,17 +19,18 @@ static TENANT: &str = "<TENANT>"; // go to in order to enter the code. Polling will continue until either the user // has entered the code. Once a successful code has been entered the next time the // device code endpoint is polled an access token is returned. -fn poll_device_code() { +fn poll_device_code() -> anyhow::Result<()> { let mut device_executor = PublicClientApplication::builder(CLIENT_ID) .with_device_code_executor() .with_scope(vec!["User.Read"]) .with_tenant(TENANT) - .poll() - .unwrap(); + .poll()?; while let Ok(response) = device_executor.recv() { println!("{:#?}", response); } + + Ok(()) } fn get_token(device_code: &str) { diff --git a/examples/oauth/environment_credential.rs b/examples/oauth/environment_credential.rs index 473dfaea..2922c102 100644 --- a/examples/oauth/environment_credential.rs +++ b/examples/oauth/environment_credential.rs @@ -1,4 +1,5 @@ use graph_oauth::oauth::EnvironmentCredential; +use graph_rs_sdk::GraphClient; use std::env::VarError; // EnvironmentCredential will first look for compile time environment variables @@ -12,16 +13,16 @@ use std::env::VarError; // "AZURE_CLIENT_ID" (Required) // "AZURE_USERNAME" (Required) // "AZURE_PASSWORD" (Required) -pub fn username_password() -> Result<(), VarError> { +pub fn username_password() -> anyhow::Result<GraphClient> { let public_client = EnvironmentCredential::resource_owner_password_credential()?; - Ok(()) + Ok(GraphClient::from(&public_client)) } // Client Secret Credentials Environment Variables: -// "AZURE_TENANT_ID" (Optional - puts the tenant id in the authorization url) +// "AZURE_TENANT_ID" (Optional/Recommended - puts the tenant id in the authorization url) // "AZURE_CLIENT_ID" (Required) // "AZURE_CLIENT_SECRET" (Required) -pub fn client_secret_credential() -> Result<(), VarError> { +pub fn client_secret_credential() -> anyhow::Result<GraphClient> { let confidential_client = EnvironmentCredential::client_secret_credential()?; - Ok(()) + Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/oauth/openid/server_examples/openid.rs b/examples/oauth/openid/server_examples/openid.rs index 895dfe14..bb4ef6c8 100644 --- a/examples/oauth/openid/server_examples/openid.rs +++ b/examples/oauth/openid/server_examples/openid.rs @@ -4,6 +4,7 @@ use graph_rs_sdk::oauth::{ }; use url::Url; +use graph_rs_sdk::GraphClient; /// # Example /// ``` /// use graph_rs_sdk::oauth::{AccessToken, IdToken, OAuth}; @@ -43,9 +44,20 @@ fn openid_authorization_url() -> anyhow::Result<Url> { .url()?) } +async fn list_users(confidential_client: &ConfidentialClientApplication<OpenIdCredential>) { + let graph_client = GraphClient::from(confidential_client); + + let response = graph_client.users().list_user().send().await.unwrap(); + + debug!("{response:#?}"); + + let users: serde_json::Value = response.json().await.unwrap(); + debug!("{:#?}", users); +} + async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, warp::Rejection> { id_token.enable_pii_logging(true); - println!("{id_token:#?}"); + debug!("{id_token:#?}"); let code = id_token.code.unwrap(); @@ -57,18 +69,7 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope .build(); - let mut response = confidential_client.execute_async().await.unwrap(); - - if response.status().is_success() { - let mut access_token: Token = response.json().await.unwrap(); - access_token.enable_pii_logging(true); - - println!("\n{access_token:#?}\n"); - } else { - // See if Microsoft Graph returned an error in the Response body - let result: reqwest::Result<serde_json::Value> = response.json().await; - println!("{result:#?}"); - } + list_users(&confidential_client); Ok(Box::new( "Successfully Logged In! You can close your browser.", diff --git a/graph-core/src/crypto/mod.rs b/graph-core/src/crypto/mod.rs index 5befae25..12037e47 100644 --- a/graph-core/src/crypto/mod.rs +++ b/graph-core/src/crypto/mod.rs @@ -4,15 +4,13 @@ pub use pkce::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; -use graph_error::{IdentityResult, AF}; use ring::rand::SecureRandom; -pub fn secure_random_32() -> IdentityResult<String> { +pub fn secure_random_32() -> String { let mut buf = [0; 32]; let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| AF::unknown("ring::error::Unspecified"))?; + rng.fill(&mut buf).expect("ring::error::Unspecified"); - Ok(URL_SAFE_NO_PAD.encode(buf)) + URL_SAFE_NO_PAD.encode(buf) } diff --git a/graph-core/src/crypto/pkce.rs b/graph-core/src/crypto/pkce.rs index 710b471f..5a681db3 100644 --- a/graph-core/src/crypto/pkce.rs +++ b/graph-core/src/crypto/pkce.rs @@ -1,6 +1,6 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use graph_error::{IdentityResult, AF}; use ring::rand::SecureRandom; /* @@ -33,17 +33,16 @@ pub trait GenPkce { /// Known as code_verifier in proof key for code exchange /// Uses the Rust ring crypto library to generate a secure random /// 32-octet sequence that is base64 URL encoded (no padding) - fn code_verifier() -> IdentityResult<String> { + fn code_verifier() -> String { let mut buf = [0; 32]; let rng = ring::rand::SystemRandom::new(); - rng.fill(&mut buf) - .map_err(|_| AuthorizationFailure::unknown("ring::error::Unspecified"))?; + rng.fill(&mut buf).expect("ring::error::Unspecified"); - Ok(URL_SAFE_NO_PAD.encode(buf)) + URL_SAFE_NO_PAD.encode(buf) } - fn code_challenge(code_verifier: &String) -> IdentityResult<String> { + fn code_challenge(code_verifier: &String) -> String { let mut context = ring::digest::Context::new(&ring::digest::SHA256); context.update(code_verifier.as_bytes()); @@ -51,7 +50,7 @@ pub trait GenPkce { let code_challenge = URL_SAFE_NO_PAD.encode(context.finish().as_ref()); // code verifier, code challenge - Ok(code_challenge) + code_challenge } /// Generate a code challenge and code verifier for the @@ -69,8 +68,8 @@ pub trait GenPkce { /// This sequence is hashed using SHA256 and base64 URL encoded (no padding) resulting in a /// 43-octet URL safe string which is known as the code challenge. fn oneshot() -> IdentityResult<ProofKeyCodeExchange> { - let code_verifier = ProofKeyCodeExchange::code_verifier()?; - let code_challenge = ProofKeyCodeExchange::code_challenge(&code_verifier)?; + let code_verifier = ProofKeyCodeExchange::code_verifier(); + let code_challenge = ProofKeyCodeExchange::code_challenge(&code_verifier); ProofKeyCodeExchange::new( code_verifier, code_challenge, diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 462cd411..5953a8a3 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -20,24 +20,13 @@ pub enum AuthorizationFailure { Uuid(#[from] uuid::Error), #[error("{0:#?}")] - Unknown(String), - - #[error("{0:#?}")] - OpenSsl(String), + Openssl(String), #[error("{0:#?}")] SerdeJson(#[from] serde_json::Error), } impl AuthorizationFailure { - pub fn unknown<T: ToString>(value: T) -> AuthorizationFailure { - AuthorizationFailure::Unknown(value.to_string()) - } - - pub fn unknown_result<T: ToString>(value: T) -> IdentityResult<AuthorizationFailure> { - Err(AuthorizationFailure::unknown(value)) - } - pub fn required<T: AsRef<str>>(name: T) -> AuthorizationFailure { AuthorizationFailure::RequiredValue { name: name.as_ref().to_owned(), @@ -89,11 +78,11 @@ impl AuthorizationFailure { } pub fn x509(message: impl ToString) -> AuthorizationFailure { - AuthorizationFailure::OpenSsl(message.to_string()) + AuthorizationFailure::Openssl(message.to_string()) } pub fn x509_result<T>(message: impl ToString) -> Result<T, AuthorizationFailure> { - Err(AuthorizationFailure::OpenSsl(message.to_string())) + Err(AuthorizationFailure::Openssl(message.to_string())) } } diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index 6e5c722c..48e58a84 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -140,13 +140,7 @@ impl From<AuthExecutionError> for GraphFailure { error.to_string() ), }, - AuthorizationFailure::Unknown(message) => GraphFailure::PreFlightError { - url: None, - headers: None, - error: None, - message, - }, - AuthorizationFailure::OpenSsl(message) => GraphFailure::PreFlightError { + AuthorizationFailure::Openssl(message) => GraphFailure::PreFlightError { url: None, headers: None, error: None, diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index 70a4490e..b8fc8d0f 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -46,7 +46,7 @@ pub enum WebViewDeviceCodeError { /// Webview Window closed for one of the following reasons: /// 1. The user closed the webview window without logging in. /// 2. The webview exited because of a timeout defined in the WebViewOptions. - #[error("WindowClosed: {0:#?}")] + #[error("window closed reason: {0:#?}")] WindowClosed(String), /// Error that happens calling the http request. #[error("{0:#?}")] diff --git a/graph-oauth/README.md b/graph-oauth/README.md index dc6286c4..aef99054 100644 --- a/graph-oauth/README.md +++ b/graph-oauth/README.md @@ -2,6 +2,7 @@ Support for: +- OpenId, Auth Code Grant, Client Credentials, Device Code - Automatic Token Refresh - Interactive Authentication | features = [`interactive-auth`] - Device Code Polling @@ -11,6 +12,19 @@ Purpose built as OAuth client for Microsoft Graph and the [graph-rs-sdk](https:/ This project can however be used outside [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) as an OAuth client for Microsoft Identity Platform or by using [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk). +## Table Of Contents + +* [Overview](#overview) +* [Credentials](#credentials) + * [Authorization Code Grant](#authorization-code-grant) + * [Client Credentials](#client-credentials) + * [Client Secret Credential](#client-secret-credential) + * [Environment Credentials](#environment-credentials) + * [Client Secret Environment Credential](#client-secret-environment-credential) + * [Resource Owner Password Credential](#resource-owner-password-credential) +* [Automatic Token Refresh](#automatic-token-refresh) +* [Interactive Authentication](#interactive-authentication) + For async: ```toml @@ -39,7 +53,10 @@ For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -## OAuth - Getting Access Tokens +## Overview + +The following examples use the `anyhow` crate for its Result type. It is also recommended that users +of this crate use the `anyhow` crate for better error handling. The crate is undergoing major development in order to support all or most scenarios in the Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some @@ -70,21 +87,7 @@ let confidental_client: ConfidentialClientApplication<ClientSecretCredential> = let graph_client = Graph::from(confidential_client); ``` -### Identity Platform Support - -The following flows from the Microsoft Identity Platform are supported: - -- [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) -- [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) -- [Authorization Code Grant Certificate](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) -- [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) -- [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) -- [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) -- [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) - -You can use the url builders for those flows that require an authorization code using a redirect after sign in you can use - -### Examples +## Credentials ### Authorization Code Grant @@ -101,33 +104,27 @@ use graph_rs_sdk::{ oauth::ConfidentialClientApplication, }; -#[tokio::main] -async fn main() { - let authorization_code = "<AUTH_CODE>"; - let client_id = "<CLIENT_ID>"; - let client_secret = "<CLIENT_SECRET>"; - let scope = vec!["<SCOPE>", "<SCOPE>"]; - let redirect_uri = "http://localhost:8080"; - +async fn build_client( + authorization_code: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str> +) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() + .with_redirect_uri(redirect_uri)? .build(); - let graph_client = Graph::from(confidential_client); + let graph_client = Graph::from(confidential_client); - let _response = graph_client - .users() - .list_user() - .send() // Also makes first access token request at this point - .await; + Ok(graph_client) } ``` -### Client Credentials Grant. +## Client Credentials The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, instead of impersonating a user, to authenticate when calling another web service. The grant specified in RFC 6749, @@ -139,27 +136,55 @@ Client credentials flow requires a one time administrator acceptance of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) +### Client Secret Credential + ```rust -use graph_rs_sdk::{ - oauth::ConfidentialClientApplication, Graph -}; +use graph_rs_sdk::{oauth::ConfidentialClientApplication, GraphClient}; + +pub async fn build_client(client_id: &str, client_secret: &str, tenant: &str) -> GraphClient { + let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) + .with_client_secret(client_secret) + .with_tenant(tenant) + .build(); + + GraphClient::from(&confidential_client_application) +} +``` + +### Environment Credentials + +#### Client Secret Environment Credential -static CLIENT_ID: &str = "<CLIENT_ID>"; -static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static TENANT_ID: &str = "<TENANT_ID>"; +Environment Variables: -pub async fn get_graph_client() -> Graph { - let mut confidential_client_application = ConfidentialClientApplication::builder(CLIENT_ID) - .with_client_secret(CLIENT_SECRET) - .with_tenant(TENANT_ID) - .build(); +- AZURE_TENANT_ID (Optional/Recommended - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_CLIENT_SECRET (Required) - Graph::from(confidential_client_application) +```rust +pub fn client_secret_credential() -> anyhow::Result<GraphClient> { + let confidential_client = EnvironmentCredential::client_secret_credential()?; + Ok(GraphClient::from(&confidential_client)) } ``` +#### Resource Owner Password Credential + +Environment Variables: + +- AZURE_TENANT_ID (Optional - puts the tenant id in the authorization url) +- AZURE_CLIENT_ID (Required) +- AZURE_USERNAME (Required) +- AZURE_PASSWORD (Required) + +```rust +pub fn username_password() -> anyhow::Result<GraphClient> { + let public_client = EnvironmentCredential::resource_owner_password_credential()?; + Ok(GraphClient::from(&public_client)) +} +``` -### Automatic Token Refresh +## Automatic Token Refresh Using automatic token refresh requires getting a refresh token as part of the token response. To get a refresh token you must include the `offline_access` scope. @@ -172,23 +197,20 @@ Tokens will still be automatically refreshed as this flow does not require using a new access token. ```rust -async fn authenticate() { +async fn authenticate(client_id: &str, tenant: &str, redirect_uri: &str) { let scope = vec!["offline_access"]; - let mut credential_builder = ConfidentialClientApplication::builder(CLIENT_ID) + + let mut credential_builder = ConfidentialClientApplication::builder(client_id) .auth_code_url_builder() - .interactive_authentication(None) // Open web view for interactive authentication sign in - .unwrap(); + .with_tenant(tenant) + .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .url(); // ... add any other parameters you need - - let confidential_client = credential_builder.with_client_secret(CLIENT_SECRET) - .build(); - - let client = Graph::from(&confidential_client); } ``` - -### Interactive Authentication +## Interactive Authentication Requires Feature `interactive_auth` @@ -201,46 +223,31 @@ Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) cra platforms that support it such as on a desktop. ```rust -use graph_rs_sdk::oauth::{ - web::Theme, web::WebViewOptions, AuthorizationCodeCredential, - ConfidentialClientApplication -}; -use graph_rs_sdk::Graph; - -fn run_interactive_auth() -> ConfidentialClientApplication<AuthorizationCodeCredential> { - let mut confidential_client_builder = ConfidentialClientApplication::builder(CLIENT_ID) - .auth_code_url_builder() - .with_tenant(TENANT_ID) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .interactive_authentication(None) - .unwrap(); - - confidential_client_builder.with_client_secret(CLIENT_SECRET).build() +use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; + +async fn authenticate( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, + scope: Vec<&str>, +) -> anyhow::Result<GraphClient> { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + + let (authorization_query_response, mut credential_builder) = + AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri) + .with_interactive_authentication_for_secret(Default::default()) + .unwrap(); + + debug!("{authorization_query_response:#?}"); + + let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + + Ok(GraphClient::from(&confidential_client)) } -async fn authenticate() { - // Create a tracing subscriber to log debug/trace events coming from - // authorization http calls and the Graph client. - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - .with_max_level(tracing::Level::TRACE) - .init(); - - let mut confidential_client = run_interactive_auth(); - - let client = Graph::from(&confidential_client); - - let response = client.user(USER_ID) - .get_user() - .send() - .await - .unwrap(); - - println!("{response:#?}"); - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); -} ``` diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index d97bcc04..5529a7cb 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,10 +1,10 @@ +use serde::Deserializer; +use serde_json::Value; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; - -use serde_json::Value; use url::Url; -/// The specification defines theres errors here: +/// The specification defines errors here: /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-31#section-4.2.2.1 /// /// Microsoft has additional errors listed here: @@ -84,11 +84,27 @@ impl Display for AuthorizationQueryError { } } +fn deserialize_expires_in<'de, D>(expires_in: D) -> Result<Option<i64>, D::Error> +where + D: Deserializer<'de>, +{ + let expires_in_string_result: Result<String, D::Error> = + serde::Deserialize::deserialize(expires_in); + if let Ok(expires_in_string) = expires_in_string_result { + if let Ok(expires_in) = expires_in_string.parse::<i64>() { + return Ok(Some(expires_in)); + } + } + + Ok(None) +} + #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct AuthorizationQueryResponse { +pub struct AuthorizationResponse { pub code: Option<String>, pub id_token: Option<String>, - pub expires_in: Option<String>, + #[serde(deserialize_with = "deserialize_expires_in")] + pub expires_in: Option<i64>, pub access_token: Option<String>, pub state: Option<String>, pub session_state: Option<String>, @@ -102,10 +118,10 @@ pub struct AuthorizationQueryResponse { log_pii: bool, } -impl AuthorizationQueryResponse { +impl AuthorizationResponse { /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled - /// passing [AuthorizationQueryResponse] to logging or print functions will log both the bearer + /// passing [AuthorizationResponse] to logging or print functions will log both the bearer /// access token value of amy and the id token value. /// By default these do not get logged. pub fn enable_pii_logging(&mut self, log_pii: bool) { @@ -117,7 +133,7 @@ impl AuthorizationQueryResponse { } } -impl Debug for AuthorizationQueryResponse { +impl Debug for AuthorizationResponse { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.log_pii { f.debug_struct("AuthQueryResponse") @@ -146,3 +162,28 @@ impl Debug for AuthorizationQueryResponse { } } } + +#[cfg(test)] +mod test { + use super::*; + + pub const AUTHORIZATION_RESPONSE: &str = r#"{ + "access_token": "token", + "expires_in": "3600" + }"#; + + #[test] + pub fn deserialize_authorization_response_from_json() { + let response: AuthorizationResponse = serde_json::from_str(AUTHORIZATION_RESPONSE).unwrap(); + assert_eq!(Some(String::from("token")), response.access_token); + assert_eq!(Some(3600), response.expires_in); + } + + #[test] + pub fn deserialize_authorization_response_from_query() { + let query = "access_token=token&expires_in=3600"; + let response: AuthorizationResponse = serde_urlencoded::from_str(query).unwrap(); + assert_eq!(Some(String::from("token")), response.access_token); + assert_eq!(Some(3600), response.expires_in); + } +} diff --git a/graph-oauth/src/identity/authorization_response.rs b/graph-oauth/src/identity/authorization_response.rs deleted file mode 100644 index 0ac0c2ec..00000000 --- a/graph-oauth/src/identity/authorization_response.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// Representation of the authorization response described in the [specification](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2) -/// -/// -/// The [specification](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1.2) states: -/// If the resource owner grants the access request, the authorization -/// server issues an authorization code and delivers it to the client by -/// adding the following parameters to the query component of the -/// redirection URI using the "application/x-www-form-urlencoded" -pub struct AuthorizationResponse { - pub code: String, - pub state: String, -} diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 4857b969..80192440 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -24,7 +24,7 @@ use crate::identity::{AuthorizationCodeCertificateCredentialBuilder, X509Certifi use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationQueryResponse, Token}; +use crate::identity::{AuthorizationResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ @@ -61,9 +61,10 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// to build the url that the user will be directed to authorize at. /// /// ```rust -/// # use graph_oauth::oauth::{ConfidentialClientApplication, Prompt}; +/// use uuid::Uuid; +/// use graph_oauth::oauth::{AzureCloudInstance, ConfidentialClientApplication, Prompt}; /// -/// let client_app = ConfidentialClientApplication::builder("client-id") +/// let auth_url_builder = ConfidentialClientApplication::builder(Uuid::new_v4().to_string()) /// .auth_code_url_builder() /// .with_tenant("tenant-id") /// .with_prompt(Prompt::Login) @@ -72,6 +73,9 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// .with_redirect_uri("http://localhost:8000") /// .build(); /// +/// let url = auth_url_builder.url(); +/// // or +/// let url = auth_url_builder.url_with_host(&AzureCloudInstance::AzurePublic); /// ``` #[derive(Clone)] pub struct AuthCodeAuthorizationUrlParameters { @@ -224,23 +228,21 @@ impl AuthCodeAuthorizationUrlParameters { } #[cfg(feature = "interactive-auth")] - #[tracing::instrument] pub fn interactive_webview_authentication( &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<AuthorizationQueryResponse> { + options: WebViewOptions, + ) -> WebViewResult<AuthorizationResponse> { let uri = self .url() .map_err(|err| Box::new(AuthExecutionError::from(err)))?; let redirect_uri = self.redirect_uri().cloned().unwrap(); - let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { AuthCodeAuthorizationUrlParameters::interactive_auth( uri, vec![redirect_uri], - web_view_options, + options, sender, ) .unwrap(); @@ -267,7 +269,7 @@ impl AuthCodeAuthorizationUrlParameters { uri.to_string() )))?; - let response_query: AuthorizationQueryResponse = + let response_query: AuthorizationResponse = serde_urlencoded::from_str(query) .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; @@ -419,7 +421,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { if let Some(nonce) = self.nonce.as_ref() { serializer.nonce(nonce); } else { - serializer.nonce(&secure_random_32()?); + serializer.nonce(&secure_random_32()); } if let Some(code_challenge) = self.code_challenge.as_ref() { @@ -604,22 +606,19 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } #[cfg(feature = "interactive-auth")] - pub fn with_interactive_authentication( + pub fn with_interactive_authentication_for_secret( &self, - options: Option<WebViewOptions>, - ) -> WebViewResult<( - AuthorizationQueryResponse, - AuthorizationCodeCredentialBuilder, - )> { + options: WebViewOptions, + ) -> WebViewResult<(AuthorizationResponse, AuthorizationCodeCredentialBuilder)> { let query_response = self .credential .interactive_webview_authentication(options)?; if let Some(authorization_code) = query_response.code.as_ref() { Ok(( - query_response.clone(), + query_response, AuthorizationCodeCredentialBuilder::new_with_auth_code( self.credential.app_config.clone(), - authorization_code, + authorization_code.clone(), ), )) } else { @@ -633,6 +632,70 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } } + #[cfg(feature = "interactive-auth")] + pub fn with_interactive_authentication_for_assertion( + &self, + options: WebViewOptions, + ) -> WebViewResult<( + AuthorizationResponse, + AuthorizationCodeAssertionCredentialBuilder, + )> { + let query_response = self + .credential + .interactive_webview_authentication(options)?; + if let Some(authorization_code) = query_response.code.as_ref() { + Ok(( + query_response, + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + self.credential.app_config.clone(), + authorization_code.clone(), + ), + )) + } else { + Ok(( + query_response.clone(), + AuthorizationCodeAssertionCredentialBuilder::new_with_token( + self.credential.app_config.clone(), + Token::from(query_response), + ), + )) + } + } + + #[cfg(feature = "interactive-auth")] + #[cfg(feature = "openssl")] + pub fn with_interactive_authentication_for_certificate( + &self, + options: WebViewOptions, + x509: &X509Certificate, + ) -> WebViewResult<( + AuthorizationResponse, + AuthorizationCodeCertificateCredentialBuilder, + )> { + let query_response = self + .credential + .interactive_webview_authentication(options)?; + if let Some(authorization_code) = query_response.code.as_ref() { + Ok(( + query_response, + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + self.credential.app_config.clone(), + authorization_code.clone(), + x509, + )?, + )) + } else { + Ok(( + query_response.clone(), + AuthorizationCodeCertificateCredentialBuilder::new_with_token( + self.credential.app_config.clone(), + Token::from(query_response), + x509, + )?, + )) + } + } + pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { self.credential.clone() } @@ -645,7 +708,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self.credential.url() } - pub fn into_credential( + pub fn into_secret_credential_builder( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { @@ -655,7 +718,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { ) } - pub fn into_assertion_credential( + pub fn into_assertion_credential_builder( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { @@ -666,7 +729,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } #[cfg(feature = "openssl")] - pub fn into_certificate_credential( + pub fn into_certificate_credential_builder( self, authorization_code: impl AsRef<str>, x509: &X509Certificate, diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 555f35ce..0b9dec81 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -345,6 +345,28 @@ impl AuthorizationCodeAssertionCredentialBuilder { } } + #[cfg(feature = "interactive-auth")] + pub(crate) fn new_with_token( + app_config: AppConfig, + token: Token, + ) -> AuthorizationCodeAssertionCredentialBuilder { + let cache_id = app_config.cache_id.clone(); + let mut token_cache = InMemoryCacheStore::new(); + token_cache.store(cache_id, token); + + Self { + credential: AuthorizationCodeAssertionCredential { + app_config, + authorization_code: None, + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + token_cache, + }, + } + } + pub(crate) fn new_with_auth_code_and_assertion( app_config: AppConfig, authorization_code: impl AsRef<str>, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 7a203c11..3918c5e7 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -353,6 +353,33 @@ impl AuthorizationCodeCertificateCredentialBuilder { Ok(builder) } + #[cfg(feature = "interactive-auth")] + #[cfg(feature = "openssl")] + pub(crate) fn new_with_token( + app_config: AppConfig, + token: Token, + x509: &X509Certificate, + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + let cache_id = app_config.cache_id.clone(); + let mut token_cache = InMemoryCacheStore::new(); + token_cache.store(cache_id, token); + + let mut builder = Self { + credential: AuthorizationCodeCertificateCredential { + app_config, + authorization_code: None, + refresh_token: None, + code_verifier: None, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), + client_assertion: String::new(), + token_cache, + }, + }; + + builder.with_x509(x509)?; + Ok(builder) + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index af093d2b..0e50303a 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -89,20 +89,20 @@ impl ImplicitCredential { pub fn new<U: ToString, I: IntoIterator<Item = U>>( client_id: impl AsRef<str>, scope: I, - ) -> IdentityResult<ImplicitCredential> { - Ok(ImplicitCredential { + ) -> ImplicitCredential { + ImplicitCredential { app_config: AppConfig::builder(client_id.as_ref()).scope(scope).build(), response_type: vec![ResponseType::Code], response_mode: ResponseMode::Query, state: None, - nonce: secure_random_32()?, + nonce: secure_random_32(), prompt: None, login_hint: None, domain_hint: None, - }) + } } - pub fn builder(client_id: impl AsRef<str>) -> IdentityResult<ImplicitCredentialBuilder> { + pub fn builder(client_id: impl AsRef<str>) -> ImplicitCredentialBuilder { ImplicitCredentialBuilder::new(client_id) } @@ -208,19 +208,19 @@ pub struct ImplicitCredentialBuilder { } impl ImplicitCredentialBuilder { - pub fn new(client_id: impl AsRef<str>) -> IdentityResult<ImplicitCredentialBuilder> { - Ok(ImplicitCredentialBuilder { + pub fn new(client_id: impl AsRef<str>) -> ImplicitCredentialBuilder { + ImplicitCredentialBuilder { credential: ImplicitCredential { app_config: AppConfig::new(client_id.as_ref()), response_type: vec![ResponseType::Code], response_mode: ResponseMode::Query, state: None, - nonce: secure_random_32()?, + nonce: secure_random_32(), prompt: None, login_hint: None, domain_hint: None, }, - }) + } } pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { @@ -254,10 +254,12 @@ impl ImplicitCredentialBuilder { self } - /// A value included in the request, generated by the app, that is included in the - /// resulting id_token as a claim. The app can then verify this value to mitigate token - /// replay attacks. The value is typically a randomized, unique string that can be used - /// to identify the origin of the request. + /// A value included in the request that is included in the resulting id_token as a claim. + /// The app can then verify this value to mitigate token replay attacks. The value is + /// typically a randomized, unique string that can be used to identify the origin of + /// the request. + /// + /// To have the client generate a nonce for you use [with_nonce_generated](crate::identity::legacy::ImplicitCredentialBuilder::with_nonce_generated) pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { self.credential.nonce = nonce.as_ref().to_owned(); self @@ -267,16 +269,9 @@ impl ImplicitCredentialBuilder { /// resulting id_token as a claim. The app can then verify this value to mitigate token /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. - /// - /// The nonce is generated in the same way as generating a PKCE. - /// - /// Internally this method uses the Rust ring cyrpto library to - /// generate a secure random 32-octet sequence that is base64 URL - /// encoded (no padding). This sequence is hashed using SHA256 and - /// base64 URL encoded (no padding) resulting in a 43-octet URL safe string. - pub fn with_nonce_generated(&mut self) -> IdentityResult<&mut Self> { - self.credential.nonce = secure_random_32()?; - Ok(self) + pub fn with_nonce_generated(&mut self) -> &mut Self { + self.credential.nonce = secure_random_32(); + self } pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { @@ -324,8 +319,7 @@ mod test { #[test] fn serialize_uri() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://localhost/myapp") @@ -344,8 +338,7 @@ mod test { #[test] fn set_open_id_fragment() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) @@ -364,8 +357,7 @@ mod test { #[test] fn set_response_mode_fragment() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https://localhost:8080/myapp") @@ -383,8 +375,7 @@ mod test { #[test] fn response_type_id_token_token_serializes() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_response_mode(ResponseMode::Fragment) @@ -404,8 +395,7 @@ mod test { #[test] fn response_type_id_token_token_serializes_from_string() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(ResponseType::StringSet( vec!["id_token".to_owned(), "token".to_owned()] @@ -430,8 +420,7 @@ mod test { #[test] #[should_panic] fn response_type_id_token_panics_with_response_mode_query() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(ResponseType::IdToken) .with_redirect_uri("http://localhost:8080/myapp") @@ -448,8 +437,7 @@ mod test { #[test] #[should_panic] fn missing_scope_panic() { - let mut authorizer = - ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e").unwrap(); + let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); authorizer .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://example.com/myapp") @@ -463,7 +451,6 @@ mod test { #[test] fn generate_nonce() { let url = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") - .unwrap() .with_redirect_uri("http://localhost:8080") .unwrap() .with_client_id(Uuid::new_v4().to_string()) diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index c6b58146..fe7d7f10 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -20,7 +20,7 @@ use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_error::{WebViewError, WebViewResult}; #[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationQueryResponse, Token}; +use crate::identity::{AuthorizationResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ @@ -152,7 +152,7 @@ impl OpenIdAuthorizationUrlParameters { .build(), response_type: BTreeSet::from([ResponseType::IdToken]), response_mode: None, - nonce: secure_random_32()?, + nonce: secure_random_32(), state: None, prompt: Default::default(), domain_hint: None, @@ -160,24 +160,20 @@ impl OpenIdAuthorizationUrlParameters { }) } - fn new_with_app_config( - app_config: AppConfig, - ) -> IdentityResult<OpenIdAuthorizationUrlParameters> { - Ok(OpenIdAuthorizationUrlParameters { + fn new_with_app_config(app_config: AppConfig) -> OpenIdAuthorizationUrlParameters { + OpenIdAuthorizationUrlParameters { app_config, response_type: BTreeSet::from([ResponseType::IdToken]), response_mode: None, - nonce: secure_random_32()?, + nonce: secure_random_32(), state: None, prompt: Default::default(), domain_hint: None, login_hint: None, - }) + } } - pub fn builder( - client_id: impl TryInto<Uuid>, - ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { + pub fn builder(client_id: impl TryInto<Uuid>) -> OpenIdAuthorizationUrlParameterBuilder { OpenIdAuthorizationUrlParameterBuilder::new(client_id) } @@ -204,11 +200,10 @@ impl OpenIdAuthorizationUrlParameters { } #[cfg(feature = "interactive-auth")] - #[tracing::instrument] pub fn interactive_webview_authentication( &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<AuthorizationQueryResponse> { + web_view_options: WebViewOptions, + ) -> WebViewResult<AuthorizationResponse> { if self.response_mode.eq(&Some(ResponseMode::FormPost)) { return Err(AF::msg_err( "response_mode", @@ -217,7 +212,6 @@ impl OpenIdAuthorizationUrlParameters { } let uri = self.url()?; let redirect_uri = self.redirect_uri().cloned().unwrap(); - let web_view_options = interactive_web_view_options.unwrap_or_default(); let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { @@ -251,7 +245,7 @@ impl OpenIdAuthorizationUrlParameters { uri.to_string() )))?; - let response_query: AuthorizationQueryResponse = + let response_query: AuthorizationResponse = serde_urlencoded::from_str(query) .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; @@ -299,13 +293,6 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { return AuthorizationFailure::result("client_id"); } - if self.nonce.is_empty() { - return AuthorizationFailure::msg_result( - "nonce", - "nonce is empty - nonce is automatically generated if not updated by the caller", - ); - } - if self.app_config.scope.is_empty() || !self.app_config.scope.contains("openid") { let mut scope = self.app_config.scope.clone(); scope.insert("openid".into()); @@ -343,7 +330,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { if response_mode.eq(&ResponseMode::Query) { return Err(AF::msg_err( "response_mode", - "openid does not support ResponseMode::Query", + "openid does not support ResponseMode::Query. Use ResponseMode::Fragment or ResponseMode::FormPost", )); } @@ -395,7 +382,6 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { #[cfg(feature = "interactive-auth")] impl InteractiveAuth for OpenIdAuthorizationUrlParameters { - #[tracing::instrument] fn webview( host_options: HostOptions, window: Window, @@ -436,14 +422,12 @@ pub struct OpenIdAuthorizationUrlParameterBuilder { } impl OpenIdAuthorizationUrlParameterBuilder { - pub(crate) fn new( - client_id: impl TryInto<Uuid>, - ) -> IdentityResult<OpenIdAuthorizationUrlParameterBuilder> { - Ok(OpenIdAuthorizationUrlParameterBuilder { + pub(crate) fn new(client_id: impl TryInto<Uuid>) -> OpenIdAuthorizationUrlParameterBuilder { + OpenIdAuthorizationUrlParameterBuilder { credential: OpenIdAuthorizationUrlParameters::new_with_app_config( - AppConfig::builder(client_id).scope(vec!["openid"]).build(), - )?, - }) + AppConfig::builder(client_id).build(), + ), + } } pub(crate) fn new_with_app_config( @@ -451,8 +435,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { ) -> OpenIdAuthorizationUrlParameterBuilder { app_config.scope.insert("openid".into()); OpenIdAuthorizationUrlParameterBuilder { - credential: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config) - .expect("ring::crypto::Unspecified"), + credential: OpenIdAuthorizationUrlParameters::new_with_app_config(app_config), } } @@ -580,8 +563,8 @@ impl OpenIdAuthorizationUrlParameterBuilder { #[cfg(feature = "interactive-auth")] pub fn with_interactive_authentication( &self, - options: Option<WebViewOptions>, - ) -> WebViewResult<(AuthorizationQueryResponse, OpenIdCredentialBuilder)> { + options: WebViewOptions, + ) -> WebViewResult<(AuthorizationResponse, OpenIdCredentialBuilder)> { let query_response = self .credential .interactive_webview_authentication(options)?; diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 6d197703..e65a2cb1 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,8 +1,13 @@ use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{Authority, AzureCloudInstance, TokenCredentialExecutor}; +use crate::identity::{ + Authority, AzureCloudInstance, Token, TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, +}; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use async_trait::async_trait; -use graph_error::{IdentityResult, AF}; +use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; +use graph_core::identity::ForceTokenRefresh; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use std::collections::HashMap; use std::fmt::{Debug, Formatter}; use uuid::Uuid; @@ -25,6 +30,7 @@ pub struct ResourceOwnerPasswordCredential { /// Required /// The user's password. pub(crate) password: String, + token_cache: InMemoryCacheStore<Token>, } impl Debug for ResourceOwnerPasswordCredential { @@ -47,6 +53,7 @@ impl ResourceOwnerPasswordCredential { .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), + token_cache: Default::default(), } } @@ -62,12 +69,85 @@ impl ResourceOwnerPasswordCredential { .build(), username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), + token_cache: Default::default(), } } pub fn builder<T: AsRef<str>>(client_id: T) -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder::new(client_id) } + + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { + let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + + let new_token: Token = response.json()?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } + + async fn execute_cached_token_refresh_async( + &mut self, + cache_id: String, + ) -> AuthExecutionResult<Token> { + let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + + let new_token: Token = response.json().await?; + self.token_cache.store(cache_id, new_token.clone()); + Ok(new_token) + } +} + +#[async_trait] +impl TokenCache for ResourceOwnerPasswordCredential { + type Token = Token; + + fn get_token_silent(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) + } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + Ok(token) + } + } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh(cache_id) + } + } + + async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError> { + let cache_id = self.app_config.cache_id.to_string(); + if let Some(token) = self.token_cache.get(cache_id.as_str()) { + if token.is_expired_sub(time::Duration::minutes(5)) { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await + } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + Ok(token.clone()) + } + } else { + tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + self.execute_cached_token_refresh_async(cache_id).await + } + } + + fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + self.app_config.force_token_refresh = force_token_refresh; + } } #[async_trait] @@ -110,8 +190,12 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { self.app_config.azure_cloud_instance } + fn basic_auth(&self) -> Option<(String, String)> { + Some((self.username.clone(), self.password.clone())) + } + fn app_config(&self) -> &AppConfig { - todo!() + &self.app_config } } @@ -127,6 +211,7 @@ impl ResourceOwnerPasswordCredentialBuilder { app_config: AppConfig::new(client_id.as_ref()), username: Default::default(), password: Default::default(), + token_cache: Default::default(), }, } } @@ -141,6 +226,7 @@ impl ResourceOwnerPasswordCredentialBuilder { app_config, username: username.as_ref().to_owned(), password: password.as_ref().to_owned(), + token_cache: Default::default(), }, } } diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index c0dd313b..c8e60a0b 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -37,26 +37,6 @@ impl IdToken { } } - pub fn id_token(&mut self, id_token: &str) { - self.id_token = id_token.into(); - } - - /*pub fn jwt(&self) -> Option<JsonWebToken> { - JwtParser::parse(self.id_token.as_str()).ok() - }*/ - - pub fn code(&mut self, code: &str) { - self.code = Some(code.into()); - } - - pub fn state(&mut self, state: &str) { - self.state = Some(state.into()); - } - - pub fn session_state(&mut self, session_state: &str) { - self.session_state = Some(session_state.into()); - } - /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled /// passing an [IdToken] to logging or print functions will log id_token field. diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index 8d5c4162..f0c20e88 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -3,11 +3,9 @@ mod application_options; mod authority; mod authorization_query_response; mod authorization_request_parts; -mod authorization_response; mod authorization_url; mod credentials; mod device_authorization_response; - mod id_token; mod token; mod token_validator; @@ -23,7 +21,6 @@ pub use application_options::*; pub use authority::*; pub use authorization_query_response::*; pub use authorization_request_parts::*; -pub use authorization_response::*; pub use authorization_url::*; pub use credentials::*; pub use device_authorization_response::*; diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index d3eeab51..1c62dcc4 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::fmt; use std::ops::{Add, Sub}; -use crate::identity::{AuthorizationQueryResponse, IdToken}; +use crate::identity::{AuthorizationResponse, IdToken}; use graph_core::cache::AsBearer; use std::str::FromStr; use time::OffsetDateTime; @@ -403,8 +403,8 @@ impl Default for Token { } } -impl From<AuthorizationQueryResponse> for Token { - fn from(value: AuthorizationQueryResponse) -> Self { +impl From<AuthorizationResponse> for Token { + fn from(value: AuthorizationResponse) -> Self { Token { access_token: value.access_token.unwrap_or_default(), token_type: "Bearer".to_string(), diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index dccd24f5..b522db84 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -29,7 +29,6 @@ where proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView>; - #[tracing::instrument] fn interactive_auth( start_url: Url, redirect_uris: Vec<Url>, @@ -50,12 +49,12 @@ where } match event { - Event::NewEvents(StartCause::Init) => tracing::trace!(target: "interactive_webview", "Webview runtime started"), + Event::NewEvents(StartCause::Init) => tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Webview runtime started"), Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { start, requested_resume })).unwrap_or_default(); - tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); + tracing::debug!(target: "graph_rs_sdk::interactive_auth", "Timeout reached - closing window"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); @@ -70,7 +69,7 @@ where .. } => { sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); - tracing::trace!(target: "interactive_webview", "Window close requested by user"); + tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Window close requested by user"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); @@ -81,12 +80,12 @@ where *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri}"); + tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Matched on redirect uri: {uri}"); sender.send(InteractiveAuthEvent::ReachedRedirectUri(uri)) .unwrap_or_default(); } Event::UserEvent(UserEvents::InternalCloseWindow) => { - tracing::trace!(target: "interactive_webview", "Closing window"); + tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Closing window"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); } diff --git a/src/client/graph.rs b/src/client/graph.rs index f92e296d..2288c2cb 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -71,7 +71,10 @@ use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_core::identity::ForceTokenRefresh; -use graph_oauth::oauth::{DeviceCodeCredential, OpenIdCredential, PublicClientApplication}; +use graph_oauth::oauth::{ + DeviceCodeCredential, OpenIdCredential, PublicClientApplication, + ResourceOwnerPasswordCredential, +}; use lazy_static::lazy_static; lazy_static! { @@ -204,17 +207,14 @@ impl GraphClient { /// optional path. The path is not set by the sdk when using a custom endpoint. /// /// The scheme must be https:// and any other provided scheme will cause a panic. - /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) + /// See [Microsoft Graph Service Root Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// /// Attempting to use an invalid host will cause the client to panic. This is done /// for increased security. /// - /// Do not use a U.S. Government host endpoint without authorization and any necessary - /// clearances. - /// - /// Using any U.S. Government or China host endpoint means you should - /// expect every API call will be monitored and recorded. The U.S. Government has made it clear - /// you have no right to privacy when using any U.S. Government website or API. + /// Do not use a government host endpoint without authorization and any necessary clearances. + /// Using any government host endpoint means you should expect every API call will be monitored + /// and recorded. /// /// You should also assume China's Graph API operated by 21Vianet is being monitored /// by the Chinese government (who controls all Chinese companies and citizens). @@ -250,17 +250,14 @@ impl GraphClient { /// optional path. The path is not set by the sdk when using a custom endpoint. /// /// The scheme must be https:// and any other provided scheme will cause a panic. - /// # See [microsoft-graph-and-graph-explorer-service-root-endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) + /// See [Microsoft Graph Service Root Endpoints](https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) /// /// Attempting to use an invalid host will cause the client to panic. This is done /// for increased security. /// - /// Do not use a U.S. Government host endpoint without authorization and any necessary - /// clearances. - /// - /// Using any U.S. Government or China host endpoint means you should - /// expect every API call will be monitored and recorded. The U.S. Government has made it clear - /// you have no right to privacy when using any U.S. Government website or API. + /// Do not use a government host endpoint without authorization and any necessary clearances. + /// Using any government host endpoint means you should expect every API call will be monitored + /// and recorded. /// /// You should also assume China's Graph API operated by 21Vianet is being monitored /// by the Chinese government (who controls all Chinese companies and citizens). @@ -618,6 +615,12 @@ impl From<&PublicClientApplication<DeviceCodeCredential>> for GraphClient { } } +impl From<&PublicClientApplication<ResourceOwnerPasswordCredential>> for GraphClient { + fn from(value: &PublicClientApplication<ResourceOwnerPasswordCredential>) -> Self { + GraphClient::from_client_app(value.clone()) + } +} + #[cfg(test)] mod test { use super::*; From d9c7b16f3dc3c70476a63f7f7897e1066e1549b3 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:07:46 -0500 Subject: [PATCH 067/118] Use AppConfig for setting its fields across builders --- .../legacy/implicit_grant.rs | 20 +-- .../src/identity/credentials/app_config.rs | 54 +++++++- .../credentials/application_builder.rs | 121 ++++++++++-------- .../auth_code_authorization_url.rs | 57 +++++---- ...authorization_code_assertion_credential.rs | 48 +++++-- ...thorization_code_certificate_credential.rs | 23 +++- .../authorization_code_credential.rs | 6 +- .../credentials/client_builder_impl.rs | 32 ++--- .../confidential_client_application.rs | 2 +- .../credentials/device_code_credential.rs | 14 ++ .../credentials/legacy/implicit_credential.rs | 37 ++---- .../credentials/open_id_authorization_url.rs | 37 ++++-- .../credentials/open_id_credential.rs | 15 +++ 13 files changed, 304 insertions(+), 162 deletions(-) diff --git a/examples/oauth_authorization_url/legacy/implicit_grant.rs b/examples/oauth_authorization_url/legacy/implicit_grant.rs index 34ac3338..66ef0e6e 100644 --- a/examples/oauth_authorization_url/legacy/implicit_grant.rs +++ b/examples/oauth_authorization_url/legacy/implicit_grant.rs @@ -25,41 +25,41 @@ use std::collections::BTreeSet; use graph_rs_sdk::oauth::legacy::ImplicitCredential; use graph_rs_sdk::oauth::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; -fn oauth_implicit_flow() { - let authorizer = ImplicitCredential::builder("<YOUR_CLIENT_ID>") - .unwrap() +fn oauth_implicit_flow() -> anyhow::Result<()> { + let credential = ImplicitCredential::builder("<YOUR_CLIENT_ID>") .with_prompt(Prompt::Login) .with_response_type(ResponseType::Token) .with_response_mode(ResponseMode::Fragment) - .with_redirect_uri("https::/localhost:8080/myapp") - .unwrap() + .with_redirect_uri("https::/localhost:8080/myapp")? .with_scope(["User.Read"]) .with_nonce("678910") .build(); - let url = authorizer.url().unwrap(); + let url = credential.url()?; // Opens the default browser to the Microsoft login page. // After logging in the page will redirect and the Url // will have the access token in either the query or // the fragment of the Uri. // webbrowser crate in dev dependencies will open to default browser in the system. - webbrowser::open(url.as_str()).unwrap(); + webbrowser::open(url.as_str())?; + + Ok(()) } -fn multi_response_types() { +fn multi_response_types() -> anyhow::Result<()> { let _ = ImplicitCredential::builder("<YOUR_CLIENT_ID>") - .unwrap() .with_response_type(vec![ResponseType::Token, ResponseType::IdToken]) .build(); // Or let _ = ImplicitCredential::builder("<YOUR_CLIENT_ID>") - .unwrap() .with_response_type(ResponseType::StringSet(BTreeSet::from_iter(vec![ "token".to_string(), "id_token".to_string(), ]))) .build(); + + Ok(()) } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index c363dca7..dccc4e24 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -1,4 +1,5 @@ use base64::Engine; +use http::{HeaderName, HeaderValue}; use std::collections::{BTreeSet, HashMap}; use std::fmt::{Debug, Formatter}; @@ -166,12 +167,63 @@ impl AppConfig { self.log_pii = log_pii; } - pub fn with_authority(&mut self, authority: Authority) { + pub(crate) fn with_client_id(&mut self, client_id: impl TryInto<Uuid>) { + self.client_id = client_id.try_into().unwrap_or_default(); + } + + pub(crate) fn with_authority(&mut self, authority: Authority) { if let Authority::TenantId(tenant_id) = &authority { self.tenant_id = Some(tenant_id.clone()); } self.authority = authority; } + + pub(crate) fn with_azure_cloud_instance(&mut self, azure_cloud_instance: AzureCloudInstance) { + self.azure_cloud_instance = azure_cloud_instance; + } + + pub(crate) fn with_tenant(&mut self, tenant_id: impl AsRef<str>) { + let tenant = tenant_id.as_ref().to_string(); + self.tenant_id = Some(tenant.clone()); + self.authority = Authority::TenantId(tenant); + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub(crate) fn with_extra_query_param(&mut self, query_param: (String, String)) { + self.extra_query_parameters + .insert(query_param.0, query_param.1); + } + + /// Extends the query parameters of both the default query params and user defined params. + /// Does not overwrite default params. + pub(crate) fn with_extra_query_parameters( + &mut self, + query_parameters: HashMap<String, String>, + ) { + self.extra_query_parameters.extend(query_parameters); + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub(crate) fn with_extra_header_param<K: Into<HeaderName>, V: Into<HeaderValue>>( + &mut self, + header_name: K, + header_value: V, + ) { + self.extra_header_parameters + .insert(header_name.into(), header_value.into()); + } + + /// Extends the header parameters of both the default header params and user defined params. + /// Does not overwrite default params. + pub(crate) fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) { + self.extra_header_parameters.extend(header_parameters); + } + + pub(crate) fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) { + self.scope = scope.into_iter().map(|s| s.to_string()).collect(); + } } #[derive(Clone, Default, PartialEq)] diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index f73d943b..4d72c7d9 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -12,6 +12,7 @@ use graph_error::{IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::env::VarError; +use uuid::Uuid; #[cfg(feature = "openssl")] use crate::identity::{ @@ -24,9 +25,9 @@ pub struct ConfidentialClientApplicationBuilder { } impl ConfidentialClientApplicationBuilder { - pub fn new(client_id: impl AsRef<str>) -> Self { + pub fn new(client_id: impl TryInto<Uuid>) -> Self { ConfidentialClientApplicationBuilder { - app_config: AppConfig::new(client_id.as_ref()), + app_config: AppConfig::new(client_id), } } @@ -37,9 +38,7 @@ impl ConfidentialClientApplicationBuilder { } pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { - let tenant = tenant_id.as_ref().to_string(); - self.app_config.tenant_id = Some(tenant.clone()); - self.app_config.authority = Authority::TenantId(tenant); + self.app_config.with_tenant(tenant_id); self } @@ -52,16 +51,15 @@ impl ConfidentialClientApplicationBuilder { &mut self, azure_cloud_instance: AzureCloudInstance, ) -> &mut Self { - self.app_config.azure_cloud_instance = azure_cloud_instance; + self.app_config + .with_azure_cloud_instance(azure_cloud_instance); self } /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { - self.app_config - .extra_query_parameters - .insert(query_param.0, query_param.1); + self.app_config.with_extra_query_param(query_param); self } @@ -72,8 +70,7 @@ impl ConfidentialClientApplicationBuilder { query_parameters: HashMap<String, String>, ) -> &mut Self { self.app_config - .extra_query_parameters - .extend(query_parameters); + .with_extra_query_parameters(query_parameters); self } @@ -85,8 +82,7 @@ impl ConfidentialClientApplicationBuilder { header_value: V, ) -> &mut Self { self.app_config - .extra_header_parameters - .insert(header_name.into(), header_value.into()); + .with_extra_header_param(header_name, header_value); self } @@ -94,13 +90,12 @@ impl ConfidentialClientApplicationBuilder { /// Does not overwrite default params. pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { self.app_config - .extra_header_parameters - .extend(header_parameters); + .with_extra_header_parameters(header_parameters); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.app_config.with_scope(scope); self } @@ -126,63 +121,72 @@ impl ConfidentialClientApplicationBuilder { /// Client Credentials Using X509 Certificate #[cfg(feature = "openssl")] pub fn with_client_x509_certificate( - self, + &mut self, certificate: &X509Certificate, ) -> IdentityResult<ClientCertificateCredentialBuilder> { - ClientCertificateCredentialBuilder::new_with_certificate(certificate, self.app_config) + ClientCertificateCredentialBuilder::new_with_certificate( + certificate, + self.app_config.clone(), + ) } /// Client Credentials Using Client Secret. pub fn with_client_secret( - self, + &mut self, client_secret: impl AsRef<str>, ) -> ClientSecretCredentialBuilder { - ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) + ClientSecretCredentialBuilder::new_with_client_secret( + client_secret, + self.app_config.clone(), + ) } /// Client Credentials Using Assertion. pub fn with_client_assertion( - self, + &mut self, signed_assertion: impl AsRef<str>, ) -> ClientAssertionCredentialBuilder { ClientAssertionCredentialBuilder::new_with_signed_assertion( signed_assertion, - self.app_config, + self.app_config.clone(), ) } /// Client Credentials Authorization Url Builder pub fn with_auth_code( - self, + &mut self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new_with_auth_code(self.into(), authorization_code) + AuthorizationCodeCredentialBuilder::new_with_auth_code( + authorization_code, + self.app_config.clone(), + ) } /// Auth Code Using Assertion pub fn with_auth_code_assertion( - self, + &mut self, authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code_and_assertion( - self.into(), authorization_code, assertion, + self.app_config.clone(), ) } /// Auth Code Using X509 Certificate #[cfg(feature = "openssl")] pub fn with_authorization_code_x509_certificate( - self, + &mut self, authorization_code: impl AsRef<str>, x509: &X509Certificate, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - self.into(), authorization_code, x509, + self.app_config.clone(), ) } @@ -190,14 +194,14 @@ impl ConfidentialClientApplicationBuilder { /// Auth Code Using OpenId. pub fn with_openid( - self, + &mut self, authorization_code: impl AsRef<str>, client_secret: impl AsRef<str>, ) -> OpenIdCredentialBuilder { OpenIdCredentialBuilder::new_with_auth_code_and_secret( authorization_code, client_secret, - self.app_config, + self.app_config.clone(), ) } } @@ -255,9 +259,7 @@ impl PublicClientApplicationBuilder { } pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { - let tenant = tenant_id.as_ref().to_string(); - self.app_config.tenant_id = Some(tenant.clone()); - self.app_config.authority = Authority::TenantId(tenant); + self.app_config.with_tenant(tenant_id); self } @@ -270,16 +272,15 @@ impl PublicClientApplicationBuilder { &mut self, azure_cloud_instance: AzureCloudInstance, ) -> &mut Self { - self.app_config.azure_cloud_instance = azure_cloud_instance; + self.app_config + .with_azure_cloud_instance(azure_cloud_instance); self } /// Extends the query parameters of both the default query params and user defined params. /// Does not overwrite default params. pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { - self.app_config - .extra_query_parameters - .insert(query_param.0, query_param.1); + self.app_config.with_extra_query_param(query_param); self } @@ -290,8 +291,7 @@ impl PublicClientApplicationBuilder { query_parameters: HashMap<String, String>, ) -> &mut Self { self.app_config - .extra_query_parameters - .extend(query_parameters); + .with_extra_query_parameters(query_parameters); self } @@ -303,8 +303,7 @@ impl PublicClientApplicationBuilder { header_value: V, ) -> &mut Self { self.app_config - .extra_header_parameters - .insert(header_name.into(), header_value.into()); + .with_extra_header_param(header_name, header_value); self } @@ -312,33 +311,38 @@ impl PublicClientApplicationBuilder { /// Does not overwrite default params. pub fn with_extra_header_parameters(&mut self, header_parameters: HeaderMap) -> &mut Self { self.app_config - .extra_header_parameters - .extend(header_parameters); + .with_extra_header_parameters(header_parameters); self } pub fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) -> &mut Self { - self.app_config.scope = scope.into_iter().map(|s| s.to_string()).collect(); + self.app_config.with_scope(scope); self } - pub fn with_device_code_executor(self) -> DeviceCodePollingExecutor { - DeviceCodePollingExecutor::new_with_app_config(self.app_config) + pub fn with_device_code_executor(&mut self) -> DeviceCodePollingExecutor { + DeviceCodePollingExecutor::new_with_app_config(self.app_config.clone()) } - pub fn with_device_code(self, device_code: impl AsRef<str>) -> DeviceCodeCredentialBuilder { - DeviceCodeCredentialBuilder::new_with_device_code(device_code.as_ref(), self.app_config) + pub fn with_device_code( + &mut self, + device_code: impl AsRef<str>, + ) -> DeviceCodeCredentialBuilder { + DeviceCodeCredentialBuilder::new_with_device_code( + device_code.as_ref(), + self.app_config.clone(), + ) } pub fn with_username_password( - self, + &mut self, username: impl AsRef<str>, password: impl AsRef<str>, ) -> ResourceOwnerPasswordCredentialBuilder { ResourceOwnerPasswordCredentialBuilder::new_with_username_password( username.as_ref(), password.as_ref(), - self.app_config, + self.app_config.clone(), ) } @@ -381,7 +385,7 @@ mod test { use url::Url; use uuid::Uuid; - use crate::identity::{AadAuthorityAudience, AzureCloudInstance}; + use crate::identity::{AadAuthorityAudience, AzureCloudInstance, TokenCredentialExecutor}; use super::*; @@ -469,4 +473,19 @@ mod test { &String::from("123") ); } + + #[test] + fn confidential_client_builder() { + let client_id = Uuid::new_v4(); + let confidential_client = ConfidentialClientApplicationBuilder::new(client_id) + .with_tenant("tenant-id") + .with_client_secret("client-secret") + .with_scope(vec!["scope"]) + .build(); + + assert_eq!( + confidential_client.client_id().to_string(), + client_id.to_string() + ); + } } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 80192440..c4b50056 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -64,7 +64,7 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// use uuid::Uuid; /// use graph_oauth::oauth::{AzureCloudInstance, ConfidentialClientApplication, Prompt}; /// -/// let auth_url_builder = ConfidentialClientApplication::builder(Uuid::new_v4().to_string()) +/// let auth_url_builder = ConfidentialClientApplication::builder(Uuid::new_v4()) /// .auth_code_url_builder() /// .with_tenant("tenant-id") /// .with_prompt(Prompt::Login) @@ -175,7 +175,7 @@ impl AuthCodeAuthorizationUrlParameters { }) } - pub fn builder<T: AsRef<str>>(client_id: T) -> AuthCodeAuthorizationUrlParameterBuilder { + pub fn builder(client_id: impl TryInto<Uuid>) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } @@ -191,7 +191,7 @@ impl AuthCodeAuthorizationUrlParameters { self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new_with_auth_code(self.app_config, authorization_code) + AuthorizationCodeCredentialBuilder::new_with_auth_code(authorization_code, self.app_config) } pub fn into_assertion_credential( @@ -211,9 +211,9 @@ impl AuthCodeAuthorizationUrlParameters { x509: &X509Certificate, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - self.app_config, authorization_code, x509, + self.app_config, ) } @@ -420,8 +420,6 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { if let Some(nonce) = self.nonce.as_ref() { serializer.nonce(nonce); - } else { - serializer.nonce(&secure_random_32()); } if let Some(code_challenge) = self.code_challenge.as_ref() { @@ -463,12 +461,12 @@ pub struct AuthCodeAuthorizationUrlParameterBuilder { } impl AuthCodeAuthorizationUrlParameterBuilder { - pub fn new<T: AsRef<str>>(client_id: T) -> AuthCodeAuthorizationUrlParameterBuilder { + pub fn new(client_id: impl TryInto<Uuid>) -> AuthCodeAuthorizationUrlParameterBuilder { let mut response_type = BTreeSet::new(); response_type.insert(ResponseType::Code); AuthCodeAuthorizationUrlParameterBuilder { credential: AuthCodeAuthorizationUrlParameters { - app_config: AppConfig::new(client_id.as_ref()), + app_config: AppConfig::new(client_id), response_mode: None, response_type, nonce: None, @@ -538,14 +536,21 @@ impl AuthCodeAuthorizationUrlParameterBuilder { /// resulting id_token as a claim. The app can then verify this value to mitigate token /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. - /// - /// Setting the nonce will override the nonce that is automatically generated by the - /// credential client. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { self.credential.nonce = Some(nonce.as_ref().to_owned()); self } + /// Generates a secure random nonce. + /// Nonce is a value included in the request, generated by the app, that is included in the + /// resulting id_token as a claim. The app can then verify this value to mitigate token + /// replay attacks. The value is typically a randomized, unique string that can be used + /// to identify the origin of the request. + pub fn with_generated_nonce(&mut self) -> &mut Self { + self.credential.nonce = Some(secure_random_32()); + self + } + pub fn with_state<T: AsRef<str>>(&mut self, state: T) -> &mut Self { self.credential.state = Some(state.as_ref().to_owned()); self @@ -615,10 +620,10 @@ impl AuthCodeAuthorizationUrlParameterBuilder { .interactive_webview_authentication(options)?; if let Some(authorization_code) = query_response.code.as_ref() { Ok(( - query_response, + query_response.clone(), AuthorizationCodeCredentialBuilder::new_with_auth_code( + authorization_code, self.credential.app_config.clone(), - authorization_code.clone(), ), )) } else { @@ -645,10 +650,10 @@ impl AuthCodeAuthorizationUrlParameterBuilder { .interactive_webview_authentication(options)?; if let Some(authorization_code) = query_response.code.as_ref() { Ok(( - query_response, + query_response.clone(), AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( self.credential.app_config.clone(), - authorization_code.clone(), + authorization_code, ), )) } else { @@ -677,11 +682,11 @@ impl AuthCodeAuthorizationUrlParameterBuilder { .interactive_webview_authentication(options)?; if let Some(authorization_code) = query_response.code.as_ref() { Ok(( - query_response, + query_response.clone(), AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - self.credential.app_config.clone(), - authorization_code.clone(), + authorization_code, x509, + self.credential.app_config.clone(), )?, )) } else { @@ -713,8 +718,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { AuthorizationCodeCredentialBuilder::new_with_auth_code( - self.credential.app_config, authorization_code, + self.credential.app_config, ) } @@ -735,9 +740,9 @@ impl AuthCodeAuthorizationUrlParameterBuilder { x509: &X509Certificate, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - self.credential.app_config, authorization_code, x509, + self.credential.app_config, ) } } @@ -748,7 +753,7 @@ mod test { #[test] fn serialize_uri() { - let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .build(); @@ -759,7 +764,7 @@ mod test { #[test] fn url_with_host() { - let url_result = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url_result = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .url_with_host(&AzureCloudInstance::AzureGermany); @@ -770,7 +775,7 @@ mod test { #[test] #[should_panic] fn response_type_id_token_panics_when_response_mode_query() { - let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .with_response_mode(ResponseMode::Query) @@ -783,7 +788,7 @@ mod test { #[test] fn response_mode_not_set() { - let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .url() @@ -796,7 +801,7 @@ mod test { #[test] fn multi_response_type_set() { - let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) @@ -811,7 +816,7 @@ mod test { #[test] fn generate_nonce() { - let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4().to_string()) + let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) .url() diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 0b9dec81..a3d02182 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -8,6 +8,7 @@ use reqwest::IntoUrl; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; @@ -59,12 +60,13 @@ impl Debug for AuthorizationCodeAssertionCredential { .finish() } } + impl AuthorizationCodeAssertionCredential { - pub fn new<T: AsRef<str>, U: IntoUrl>( - client_id: T, - authorization_code: T, - client_assertion: T, - redirect_uri: Option<U>, + pub fn new( + client_id: impl TryInto<Uuid>, + authorization_code: impl AsRef<str>, + client_assertion: impl AsRef<str>, + redirect_uri: Option<impl IntoUrl>, ) -> IdentityResult<AuthorizationCodeAssertionCredential> { let redirect_uri = { if let Some(redirect_uri) = redirect_uri { @@ -75,7 +77,7 @@ impl AuthorizationCodeAssertionCredential { }; Ok(AuthorizationCodeAssertionCredential { - app_config: AppConfig::builder(client_id.as_ref()) + app_config: AppConfig::builder(client_id) .redirect_uri_option(redirect_uri) .build(), authorization_code: Some(authorization_code.as_ref().to_owned()), @@ -88,23 +90,30 @@ impl AuthorizationCodeAssertionCredential { } pub fn builder( - client_id: impl AsRef<str>, + client_id: impl TryInto<Uuid>, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( - AppConfig::new(client_id.as_ref()), + AppConfig::new(client_id), authorization_code, ) } - pub fn authorization_url_builder<T: AsRef<str>>( - client_id: T, + pub fn authorization_url_builder( + client_id: impl TryInto<Uuid>, ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + let new_token: Token = response.json()?; self.token_cache.store(cache_id, new_token.clone()); @@ -120,6 +129,13 @@ impl AuthorizationCodeAssertionCredential { cache_id: String, ) -> AuthExecutionResult<Token> { let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + let new_token: Token = response.json().await?; if new_token.refresh_token.is_some() { @@ -328,6 +344,16 @@ pub struct AuthorizationCodeAssertionCredentialBuilder { } impl AuthorizationCodeAssertionCredentialBuilder { + pub fn new( + client_id: impl TryInto<Uuid>, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + AppConfig::new(client_id), + authorization_code, + ) + } + pub(crate) fn new_with_auth_code( app_config: AppConfig, authorization_code: impl AsRef<str>, @@ -368,9 +394,9 @@ impl AuthorizationCodeAssertionCredentialBuilder { } pub(crate) fn new_with_auth_code_and_assertion( - app_config: AppConfig, authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, + app_config: AppConfig, ) -> AuthorizationCodeAssertionCredentialBuilder { Self { credential: AuthorizationCodeAssertionCredential { diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 3918c5e7..0c7cb469 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -8,6 +8,7 @@ use reqwest::IntoUrl; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; @@ -94,20 +95,27 @@ impl AuthorizationCodeCertificateCredential { x509: &X509Certificate, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - AppConfig::new(client_id.as_ref()), authorization_code, x509, + AppConfig::new(client_id.as_ref()), ) } - pub fn authorization_url_builder<T: AsRef<str>>( - client_id: T, + pub fn authorization_url_builder( + client_id: impl TryInto<Uuid>, ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + let new_token: Token = response.json()?; self.token_cache.store(cache_id, new_token.clone()); @@ -123,6 +131,13 @@ impl AuthorizationCodeCertificateCredential { cache_id: String, ) -> AuthExecutionResult<Token> { let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + let new_token: Token = response.json().await?; if new_token.refresh_token.is_some() { @@ -333,9 +348,9 @@ pub struct AuthorizationCodeCertificateCredentialBuilder { impl AuthorizationCodeCertificateCredentialBuilder { #[cfg(feature = "openssl")] pub(crate) fn new_with_auth_code_and_x509( - app_config: AppConfig, authorization_code: impl AsRef<str>, x509: &X509Certificate, + app_config: AppConfig, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { let mut builder = Self { credential: AuthorizationCodeCertificateCredential { diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 8271e8e9..1fb4b31d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -260,8 +260,8 @@ impl AuthorizationCodeCredential { AuthorizationCodeCredentialBuilder::new(client_id, client_secret, authorization_code) } - pub fn authorization_url_builder<T: AsRef<str>>( - client_id: T, + pub fn authorization_url_builder( + client_id: impl TryInto<Uuid>, ) -> AuthCodeAuthorizationUrlParameterBuilder { AuthCodeAuthorizationUrlParameterBuilder::new(client_id) } @@ -312,8 +312,8 @@ impl AuthorizationCodeCredentialBuilder { } pub(crate) fn new_with_auth_code( - app_config: AppConfig, authorization_code: impl AsRef<str>, + app_config: AppConfig, ) -> AuthorizationCodeCredentialBuilder { Self { credential: AuthorizationCodeCredential { diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index addb42fd..47bd2f7d 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -1,23 +1,20 @@ macro_rules! credential_builder_base { ($name:ident) => { impl $name { - pub fn with_client_id(&mut self, client_id: impl AsRef<str>) -> &mut Self { - self.credential.app_config.client_id = - Uuid::try_parse(client_id.as_ref()).unwrap_or_default(); + pub fn with_client_id(&mut self, client_id: impl TryInto<uuid::Uuid>) -> &mut Self { + self.credential.app_config.with_client_id(client_id); self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] pub fn with_tenant(&mut self, tenant_id: impl AsRef<str>) -> &mut Self { - self.credential.app_config.authority = - crate::identity::Authority::TenantId(tenant_id.as_ref().to_owned()); - self.credential.app_config.tenant_id = Some(tenant_id.as_ref().to_owned()); + self.credential.app_config.with_tenant(tenant_id); self } - pub fn with_authority<T: Into<crate::identity::Authority>>( + pub fn with_authority( &mut self, - authority: T, + authority: impl Into<crate::identity::Authority>, ) -> &mut Self { self.credential.app_config.with_authority(authority.into()); self @@ -27,7 +24,9 @@ macro_rules! credential_builder_base { &mut self, azure_cloud_instance: AzureCloudInstance, ) -> &mut Self { - self.credential.app_config.azure_cloud_instance = azure_cloud_instance; + self.credential + .app_config + .with_azure_cloud_instance(azure_cloud_instance); self } @@ -36,8 +35,7 @@ macro_rules! credential_builder_base { pub fn with_extra_query_param(&mut self, query_param: (String, String)) -> &mut Self { self.credential .app_config - .extra_query_parameters - .insert(query_param.0, query_param.1); + .with_extra_query_param(query_param); self } @@ -49,8 +47,7 @@ macro_rules! credential_builder_base { ) -> &mut Self { self.credential .app_config - .extra_query_parameters - .extend(query_parameters); + .with_extra_query_parameters(query_parameters); self } @@ -63,8 +60,7 @@ macro_rules! credential_builder_base { ) -> &mut Self { self.credential .app_config - .extra_header_parameters - .insert(header_name.into(), header_value.into()); + .with_extra_header_param(header_name, header_value); self } @@ -76,8 +72,7 @@ macro_rules! credential_builder_base { ) -> &mut Self { self.credential .app_config - .extra_header_parameters - .extend(header_parameters); + .with_extra_header_parameters(header_parameters); self } @@ -85,8 +80,7 @@ macro_rules! credential_builder_base { &mut self, scope: I, ) -> &mut Self { - self.credential.app_config.scope = - scope.into_iter().map(|s| s.to_string()).collect(); + self.credential.app_config.with_scope(scope); self } } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 575addee..c5c9ffb1 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -38,7 +38,7 @@ pub struct ConfidentialClientApplication<Credential> { } impl ConfidentialClientApplication<()> { - pub fn builder(client_id: impl AsRef<str>) -> ConfidentialClientApplicationBuilder { + pub fn builder(client_id: impl TryInto<Uuid>) -> ConfidentialClientApplicationBuilder { ConfidentialClientApplicationBuilder::new(client_id) } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 39342bcc..2d2a231e 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -101,6 +101,13 @@ impl DeviceCodeCredential { fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + let new_token: Token = response.json()?; self.token_cache.store(cache_id, new_token.clone()); @@ -116,6 +123,13 @@ impl DeviceCodeCredential { cache_id: String, ) -> AuthExecutionResult<Token> { let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + let new_token: Token = response.json().await?; if new_token.refresh_token.is_some() { diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 0e50303a..66c0bbdd 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,15 +1,12 @@ +use crate::identity::credentials::app_config::AppConfig; +use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::crypto::secure_random_32; use graph_error::{AuthorizationFailure, IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; use std::collections::HashMap; - use url::Url; -use uuid::*; - -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; credential_builder_base!(ImplicitCredentialBuilder); @@ -259,17 +256,18 @@ impl ImplicitCredentialBuilder { /// typically a randomized, unique string that can be used to identify the origin of /// the request. /// - /// To have the client generate a nonce for you use [with_nonce_generated](crate::identity::legacy::ImplicitCredentialBuilder::with_nonce_generated) + /// To have the client generate a nonce for you use [with_nonce_generated](crate::identity::legacy::ImplicitCredentialBuilder::with_generated_nonce) pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { self.credential.nonce = nonce.as_ref().to_owned(); self } + /// Generates a secure random nonce. /// A value included in the request, generated by the app, that is included in the /// resulting id_token as a claim. The app can then verify this value to mitigate token /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. - pub fn with_nonce_generated(&mut self) -> &mut Self { + pub fn with_generated_nonce(&mut self) -> &mut Self { self.credential.nonce = secure_random_32(); self } @@ -319,8 +317,7 @@ mod test { #[test] fn serialize_uri() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://localhost/myapp") .unwrap() @@ -338,8 +335,7 @@ mod test { #[test] fn set_open_id_fragment() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https://localhost:8080/myapp") @@ -357,8 +353,7 @@ mod test { #[test] fn set_response_mode_fragment() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("https://localhost:8080/myapp") .unwrap() @@ -375,8 +370,7 @@ mod test { #[test] fn response_type_id_token_token_serializes() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::IdToken, ResponseType::Token]) .with_response_mode(ResponseMode::Fragment) .with_redirect_uri("http://localhost:8080/myapp") @@ -395,8 +389,7 @@ mod test { #[test] fn response_type_id_token_token_serializes_from_string() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::StringSet( vec!["id_token".to_owned(), "token".to_owned()] .into_iter() @@ -420,8 +413,7 @@ mod test { #[test] #[should_panic] fn response_type_id_token_panics_with_response_mode_query() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(ResponseType::IdToken) .with_redirect_uri("http://localhost:8080/myapp") .unwrap() @@ -437,8 +429,7 @@ mod test { #[test] #[should_panic] fn missing_scope_panic() { - let mut authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e"); - authorizer + let authorizer = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_response_type(vec![ResponseType::Token]) .with_redirect_uri("https://example.com/myapp") .unwrap() @@ -453,7 +444,7 @@ mod test { let url = ImplicitCredential::builder("6731de76-14a6-49ae-97bc-6eba6914391e") .with_redirect_uri("http://localhost:8080") .unwrap() - .with_client_id(Uuid::new_v4().to_string()) + .with_client_id(Uuid::new_v4()) .with_scope(["read", "write"]) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_response_mode(ResponseMode::Fragment) diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index fe7d7f10..f4de6b12 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -499,17 +499,11 @@ impl OpenIdAuthorizationUrlParameterBuilder { /// replay attacks. The value is typically a randomized, unique string that can be used /// to identify the origin of the request. /// - /// Because openid requires a nonce as part of the OAuth flow a nonce is already included. - /// The nonce is generated internally using the same requirements of generating a secure - /// random string as is done when using proof key for code exchange (PKCE) in the - /// authorization code grant. If you are unsure or unclear how the nonce works then it is - /// recommended to stay with the generated nonce as it is cryptographically secure. + /// Because openid requires a nonce as part of the openid flow a secure random nonce + /// is already generated for OpenIdCredential. Providing a nonce here will override this + /// generated nonce. pub fn with_nonce<T: AsRef<str>>(&mut self, nonce: T) -> &mut Self { - if self.credential.nonce.is_empty() { - self.credential.nonce.push_str(nonce.as_ref()); - } else { - self.credential.nonce = nonce.as_ref().to_owned(); - } + self.credential.nonce = nonce.as_ref().to_owned(); self } @@ -607,12 +601,12 @@ impl OpenIdAuthorizationUrlParameterBuilder { #[cfg(test)] mod test { use super::*; + use crate::identity::TokenCredentialExecutor; #[test] #[should_panic] fn panics_on_invalid_response_type_code_token() { let _ = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) - .unwrap() .with_response_type([ResponseType::Code, ResponseType::Token]) .with_scope(["scope"]) .url() @@ -623,7 +617,6 @@ mod test { #[should_panic] fn panics_on_invalid_client_id() { let _ = OpenIdAuthorizationUrlParameters::builder("client_id") - .unwrap() .with_response_type([ResponseType::Token]) .with_scope(["scope"]) .url() @@ -633,7 +626,6 @@ mod test { #[test] fn scope_openid_automatically_set() { let url = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) - .unwrap() .with_response_type([ResponseType::Code]) .with_scope(["user.read"]) .url() @@ -641,4 +633,23 @@ mod test { let query = url.query().unwrap(); assert!(query.contains("scope=openid+user.read")) } + + #[test] + fn into_credential() { + let client_id = Uuid::new_v4(); + let url_builder = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) + .with_response_type([ResponseType::Code]) + .with_scope(["user.read"]) + .build(); + let mut credential = url_builder.into_credential("code"); + let confidential_client = credential + .with_client_secret("secret") + .with_tenant("tenant") + .build(); + + assert_eq!( + confidential_client.client_id().to_string(), + client_id.to_string() + ) + } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 9c5137c8..b9b39eb5 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -9,6 +9,7 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; +use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; @@ -115,6 +116,13 @@ impl OpenIdCredential { fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response()?, + )); + } + let new_token: Token = response.json()?; self.token_cache.store(cache_id, new_token.clone()); @@ -130,6 +138,13 @@ impl OpenIdCredential { cache_id: String, ) -> AuthExecutionResult<Token> { let response = self.execute_async().await?; + + if !response.status().is_success() { + return Err(AuthExecutionError::silent_token_auth( + response.into_http_response_async().await?, + )); + } + let new_token: Token = response.json().await?; if new_token.refresh_token.is_some() { From c574c723c73490776d9d7f4a6f0ac5f61ca344a3 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:20:25 -0500 Subject: [PATCH 068/118] Change back to using tokio Mutex in tests for async aware Mutex --- examples/interactive_authentication/webview_errors.rs | 2 +- .../identity/credentials/legacy/implicit_credential.rs | 1 + test-tools/src/oauth_request.rs | 10 +++++----- tests/async_concurrency.rs | 2 +- tests/download_error.rs | 4 ++-- tests/drive_download_request.rs | 4 ++-- tests/drive_request.rs | 8 ++++---- tests/graph_error.rs | 2 +- tests/job_status.rs | 2 +- tests/mail_folder_request.rs | 4 ++-- tests/message_request.rs | 4 ++-- tests/onenote_request.rs | 6 +++--- tests/paging.rs | 4 ++-- tests/reports_request.rs | 4 ++-- tests/todo_tasks_request.rs | 2 +- tests/upload_request.rs | 4 ++-- tests/upload_session_request.rs | 2 +- tests/user_request.rs | 4 ++-- 18 files changed, 35 insertions(+), 34 deletions(-) diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_authentication/webview_errors.rs index 6808d3cb..52e52b43 100644 --- a/examples/interactive_authentication/webview_errors.rs +++ b/examples/interactive_authentication/webview_errors.rs @@ -6,7 +6,7 @@ async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, re .with_tenant(tenant_id) .with_scope(scope) .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(None); + .with_interactive_authentication_for_secret(Default::default()); if let Ok((authorization_query_response, credential_builder)) = credential_builder_result { // ... diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 66c0bbdd..5e0a6387 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -314,6 +314,7 @@ impl ImplicitCredentialBuilder { #[cfg(test)] mod test { use super::*; + use uuid::Uuid; #[test] fn serialize_uri() { diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index b69188f9..b8692bbb 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -17,11 +17,11 @@ use graph_core::identity::ClientApplication; // static mutex's that are used for preventing test failures // due to too many concurrent requests (throttling) for Microsoft Graph. lazy_static! { - pub static ref ASYNC_THROTTLE_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); - pub static ref ASYNC_THROTTLE_MUTEX2: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); - pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: parking_lot::Mutex<()> = parking_lot::Mutex::new(()); - pub static ref DRIVE_ASYNC_THROTTLE_MUTEX2: parking_lot::Mutex<()> = - parking_lot::Mutex::new(()); + pub static ref ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); + pub static ref ASYNC_THROTTLE_MUTEX2: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); + pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); + pub static ref DRIVE_ASYNC_THROTTLE_MUTEX2: tokio::sync::Mutex<()> = + tokio::sync::Mutex::new(()); } //pub const APPLICATIONS_CLIENT: Mutex<Option<(String, Graph)>> = Mutex::new(OAuthTestClient::graph_by_rid(ResourceIdentity::Applications)); diff --git a/tests/async_concurrency.rs b/tests/async_concurrency.rs index 16315c21..81f233fc 100644 --- a/tests/async_concurrency.rs +++ b/tests/async_concurrency.rs @@ -34,7 +34,7 @@ pub struct LicenseDetail { #[tokio::test] async fn buffered_requests() { - let _ = ASYNC_THROTTLE_MUTEX2.lock(); + let _ = ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut stream = client .users() diff --git a/tests/download_error.rs b/tests/download_error.rs index 198c8a06..8b4b476e 100644 --- a/tests/download_error.rs +++ b/tests/download_error.rs @@ -9,7 +9,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn download_config_file_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .user(id.as_str()) @@ -46,7 +46,7 @@ async fn download_config_file_exists() { #[tokio::test] async fn download_is_err_config_dir_no_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .user(id.as_str()) diff --git a/tests/drive_download_request.rs b/tests/drive_download_request.rs index 85984631..53072707 100644 --- a/tests/drive_download_request.rs +++ b/tests/drive_download_request.rs @@ -7,7 +7,7 @@ use test_tools::support::cleanup::AsyncCleanUp; #[tokio::test] async fn drive_download() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .drive(id.as_str()) @@ -35,7 +35,7 @@ async fn drive_download() { #[tokio::test] async fn drive_download_format() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; if Environment::is_local() { if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client diff --git a/tests/drive_request.rs b/tests/drive_request.rs index 9b52bf56..15533267 100644 --- a/tests/drive_request.rs +++ b/tests/drive_request.rs @@ -14,7 +14,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_versions_get_item() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let get_item_res = client .user(id.as_str()) @@ -48,7 +48,7 @@ async fn list_versions_get_item() { #[tokio::test] async fn drive_check_in_out() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if Environment::is_local() { if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client @@ -95,7 +95,7 @@ async fn update_item_by_path( #[tokio::test] async fn drive_update() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let req = update_item_by_path( id.as_str(), @@ -197,7 +197,7 @@ async fn delete_file( #[tokio::test] async fn drive_upload_item() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file.txt"; let file_name = ":/test_upload_file.txt:"; diff --git a/tests/graph_error.rs b/tests/graph_error.rs index 81ece60d..22ee8a9d 100644 --- a/tests/graph_error.rs +++ b/tests/graph_error.rs @@ -6,7 +6,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn drive_download_graph_error() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let response = client .drive(id.as_str()) diff --git a/tests/job_status.rs b/tests/job_status.rs index 76720272..7ca6067c 100644 --- a/tests/job_status.rs +++ b/tests/job_status.rs @@ -22,7 +22,7 @@ async fn delete_item( #[tokio::test] async fn job_status() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; let original_file = ":/test_job_status.txt:"; let copy_name = "test_job_status_copy.txt"; diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index 3c138e26..cd3d3a34 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -6,7 +6,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn get_drafts_mail_folder() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock(); + let _ = ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await { @@ -29,7 +29,7 @@ async fn get_drafts_mail_folder() { #[tokio::test] async fn mail_folder_list_messages() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock(); + let _ = ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await { diff --git a/tests/message_request.rs b/tests/message_request.rs index f8897a44..b3a588d7 100644 --- a/tests/message_request.rs +++ b/tests/message_request.rs @@ -6,7 +6,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_and_get_messages() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock(); + let _ = ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { if let Ok(response) = client .user(id.as_str()) @@ -41,7 +41,7 @@ async fn list_and_get_messages() { #[tokio::test] async fn mail_create_and_delete_message() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock(); + let _ = ASYNC_THROTTLE_MUTEX2.lock().await; if let Some((id, mut client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client .v1() diff --git a/tests/onenote_request.rs b/tests/onenote_request.rs index e77e4add..962190b7 100644 --- a/tests/onenote_request.rs +++ b/tests/onenote_request.rs @@ -15,7 +15,7 @@ async fn list_get_notebooks_and_sections() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { let notebooks = client @@ -84,7 +84,7 @@ async fn create_delete_page_from_file() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { let res = client @@ -124,7 +124,7 @@ async fn download_page() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((user_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await { diff --git a/tests/paging.rs b/tests/paging.rs index a95d5f9c..4f616af4 100644 --- a/tests/paging.rs +++ b/tests/paging.rs @@ -9,7 +9,7 @@ async fn paging_all() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut vec = client .users() @@ -33,7 +33,7 @@ async fn paging_all() { #[tokio::test] async fn paging_stream() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock(); + let _lock = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let mut stream = client .users() diff --git a/tests/reports_request.rs b/tests/reports_request.rs index 6d29c4c0..d086704f 100644 --- a/tests/reports_request.rs +++ b/tests/reports_request.rs @@ -11,7 +11,7 @@ async fn async_download_office_365_user_counts_reports_test() { return; } - let _ = ASYNC_THROTTLE_MUTEX.lock(); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Reports).await @@ -47,7 +47,7 @@ async fn async_download_office_365_user_counts_reports_test() { #[tokio::test] async fn get_office_365_user_counts_reports_text() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock(); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((_id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Reports).await diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs index 338ea51b..c3888280 100644 --- a/tests/todo_tasks_request.rs +++ b/tests/todo_tasks_request.rs @@ -19,7 +19,7 @@ struct TodoListsTasks { #[tokio::test] async fn list_users() { - let _ = ASYNC_THROTTLE_MUTEX.lock(); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::TodoListsTasks).await { diff --git a/tests/upload_request.rs b/tests/upload_request.rs index a4dc72f8..c50c7fe3 100644 --- a/tests/upload_request.rs +++ b/tests/upload_request.rs @@ -86,7 +86,7 @@ async fn get_file_content( #[tokio::test] async fn upload_bytes_mut() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; @@ -134,7 +134,7 @@ async fn upload_bytes_mut() { #[tokio::test] async fn upload_reqwest_body() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let local_file = "./test_files/test_upload_file_bytes.txt"; let file_name = ":/test_upload_file_bytes.txt:"; diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index e3368d15..f1744249 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -162,7 +162,7 @@ async fn file_upload_session_channel( // This is a long running test. 20 - 30 seconds. #[tokio::test] async fn test_upload_session() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock(); + let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let item_by_path = ":/upload_session_file.txt:"; let local_file = "./test_files/upload_session_file.txt"; diff --git a/tests/user_request.rs b/tests/user_request.rs index ce2e3c3d..e6c37873 100644 --- a/tests/user_request.rs +++ b/tests/user_request.rs @@ -5,7 +5,7 @@ use test_tools::oauth_request::{Environment, OAuthTestClient}; #[tokio::test] async fn list_users() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock(); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client.users().list_user().send().await; @@ -27,7 +27,7 @@ async fn list_users() { #[tokio::test] async fn get_user() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock(); + let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { let result = client.users().id(id).get_user().send().await; From 12458698fff61bdd3aff7d5592b81ee5b568cfbd Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:06:47 -0500 Subject: [PATCH 069/118] Create mutexes with client credentials confidential clients for tests --- .../auth_code_authorization_url.rs | 1 + .../credentials/open_id_authorization_url.rs | 4 +- test-tools/src/oauth_request.rs | 59 +++- tests/batch.rs | 124 ++++---- tests/download_error.rs | 88 +++--- tests/drive_download_request.rs | 80 +++--- tests/drive_request.rs | 252 ++++++++-------- tests/drive_url.rs | 6 +- tests/mail_folder_request.rs | 71 ++--- tests/message_request.rs | 131 +++++---- tests/onenote_request.rs | 271 +++++++++--------- tests/paging.rs | 83 +++--- tests/todo_tasks_request.rs | 2 +- tests/upload_request.rs | 158 +++++----- tests/upload_request_blocking.rs | 2 +- tests/upload_session_request.rs | 70 +++-- tests/user_request.rs | 41 --- 17 files changed, 731 insertions(+), 712 deletions(-) delete mode 100644 tests/user_request.rs diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index c4b50056..0e71a7a9 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -819,6 +819,7 @@ mod test { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) .with_redirect_uri("https://localhost:8080") .with_scope(["read", "write"]) + .with_generated_nonce() .url() .unwrap(); diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index f4de6b12..2d14c67f 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -637,7 +637,7 @@ mod test { #[test] fn into_credential() { let client_id = Uuid::new_v4(); - let url_builder = OpenIdAuthorizationUrlParameters::builder(Uuid::new_v4()) + let url_builder = OpenIdAuthorizationUrlParameters::builder(client_id) .with_response_type([ResponseType::Code]) .with_scope(["user.read"]) .build(); @@ -650,6 +650,6 @@ mod test { assert_eq!( confidential_client.client_id().to_string(), client_id.to_string() - ) + ); } } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index b8692bbb..f6607be2 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -6,7 +6,7 @@ use graph_rs_sdk::oauth::{ ClientSecretCredential, ConfidentialClientApplication, ResourceOwnerPasswordCredential, Token, TokenCredentialExecutor, }; -use graph_rs_sdk::Graph; +use graph_rs_sdk::{Graph, GraphClient}; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::env; @@ -14,6 +14,39 @@ use std::io::{Read, Write}; use graph_core::identity::ClientApplication; +pub struct GraphTestClient { + pub client: GraphClient, + pub user_id: String, +} + +impl GraphTestClient { + pub fn new_mutex() -> tokio::sync::Mutex<GraphTestClient> { + let app_registration = OAuthTestClient::get_app_registration().unwrap(); + let app_registration_client = app_registration.get_default_client_credentials(); + let test_client = app_registration_client + .clients + .get(&OAuthTestClient::ClientCredentials) + .cloned() + .unwrap(); + let user_id = test_client.user_id.clone().unwrap(); + let client = Graph::from(&test_client.client_credentials()); + tokio::sync::Mutex::new(GraphTestClient { client, user_id }) + } + + pub fn new_mutex_from_identity( + resource_identity: ResourceIdentity, + ) -> tokio::sync::Mutex<GraphTestClient> { + let app_registration = OAuthTestClient::get_app_registration().unwrap(); + let client = app_registration + .get_by_resource_identity(resource_identity) + .unwrap(); + let (test_client, credentials) = client.default_client().unwrap(); + let (user_id, client_application) = test_client.get_credential(credentials).unwrap(); + let client = GraphClient::from_client_app(client_application); + tokio::sync::Mutex::new(GraphTestClient { client, user_id }) + } +} + // static mutex's that are used for preventing test failures // due to too many concurrent requests (throttling) for Microsoft Graph. lazy_static! { @@ -22,6 +55,18 @@ lazy_static! { pub static ref DRIVE_ASYNC_THROTTLE_MUTEX: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); pub static ref DRIVE_ASYNC_THROTTLE_MUTEX2: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(()); + pub static ref DEFAULT_CLIENT_CREDENTIALS_MUTEX: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex(); + pub static ref DEFAULT_CLIENT_CREDENTIALS_MUTEX2: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex(); + pub static ref DEFAULT_CLIENT_CREDENTIALS_MUTEX3: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex(); + pub static ref DEFAULT_CLIENT_CREDENTIALS_MUTEX4: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex(); + pub static ref DEFAULT_CLIENT_CREDENTIALS_MUTEX5: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex(); + pub static ref DEFAULT_ONENOTE_CREDENTIALS_MUTEX: tokio::sync::Mutex<GraphTestClient> = + GraphTestClient::new_mutex_from_identity(ResourceIdentity::Onenote); } //pub const APPLICATIONS_CLIENT: Mutex<Option<(String, Graph)>> = Mutex::new(OAuthTestClient::graph_by_rid(ResourceIdentity::Applications)); @@ -310,6 +355,18 @@ impl OAuthTestClient { Some(confidential_client.into_inner()) } + pub fn default_graph_client() -> GraphClient { + let app_registration = OAuthTestClient::get_app_registration().unwrap(); + let app_registration_client = app_registration.get_default_client_credentials(); + let test_client = app_registration_client + .clients + .get(&OAuthTestClient::ClientCredentials) + .cloned() + .unwrap(); + let confidential_client = test_client.client_credentials(); + Graph::from(&confidential_client) + } + pub async fn graph_by_rid_async( resource_identity: ResourceIdentity, ) -> Option<(String, Graph)> { diff --git a/tests/batch.rs b/tests/batch.rs index 9064c9cc..984f15da 100644 --- a/tests/batch.rs +++ b/tests/batch.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::*; -use test_tools::oauth_request::OAuthTestClient; +use test_tools::oauth_request::DEFAULT_CLIENT_CREDENTIALS_MUTEX3; #[test] pub fn batch_url() { @@ -19,72 +19,72 @@ pub fn batch_url() { #[tokio::test] pub async fn batch_request() { - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let mut one = false; - let mut two = false; - let mut three = false; - let mut four = false; - let mut five = false; + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + let id = test_client.user_id.clone(); + let mut one = false; + let mut two = false; + let mut three = false; + let mut four = false; + let mut five = false; - let json = serde_json::json!({ - "requests": [ - { - "id": "1", - "method": "GET", - "url": format!("/users/{}/drive", id.as_str()) - }, - { - "id": "2", - "method": "GET", - "url": format!("/users/{}/drive/root", id.as_str()) - }, - { - "id": "3", - "method": "GET", - "url": format!("/users/{}/drive/recent", id.as_str()) - }, - { - "id": "4", - "method": "GET", - "url": format!("/users/{}/drive/root/children", id.as_str()) - }, - { - "id": "5", - "method": "GET", - "url": format!("/users/{}/drive/special/documents", id.as_str()) - }, - ] - }); + let json = serde_json::json!({ + "requests": [ + { + "id": "1", + "method": "GET", + "url": format!("/users/{}/drive", id.as_str()) + }, + { + "id": "2", + "method": "GET", + "url": format!("/users/{}/drive/root", id.as_str()) + }, + { + "id": "3", + "method": "GET", + "url": format!("/users/{}/drive/recent", id.as_str()) + }, + { + "id": "4", + "method": "GET", + "url": format!("/users/{}/drive/root/children", id.as_str()) + }, + { + "id": "5", + "method": "GET", + "url": format!("/users/{}/drive/special/documents", id.as_str()) + }, + ] + }); - let response = client.batch(&json).send().await.unwrap(); + let response = test_client.client.batch(&json).send().await.unwrap(); - let body: serde_json::Value = response.json().await.unwrap(); + let body: serde_json::Value = response.json().await.unwrap(); - for v in body["responses"].as_array().unwrap().iter() { - match v["id"].as_str().unwrap().as_bytes() { - b"1" => { - one = true; - } - b"2" => { - two = true; - } - b"3" => { - three = true; - } - b"4" => { - four = true; - } - b"5" => { - five = true; - } - _ => {} + for v in body["responses"].as_array().unwrap().iter() { + match v["id"].as_str().unwrap().as_bytes() { + b"1" => { + one = true; } + b"2" => { + two = true; + } + b"3" => { + three = true; + } + b"4" => { + four = true; + } + b"5" => { + five = true; + } + _ => {} } - - assert!(one); - assert!(two); - assert!(three); - assert!(four); - assert!(five); } + + assert!(one); + assert!(two); + assert!(three); + assert!(four); + assert!(five); } diff --git a/tests/download_error.rs b/tests/download_error.rs index 8b4b476e..f36d95d3 100644 --- a/tests/download_error.rs +++ b/tests/download_error.rs @@ -3,42 +3,39 @@ use graph_rs_sdk::http::FileConfig; use graph_http::traits::ResponseExt; use std::ffi::OsStr; -use test_tools::oauth_request::DRIVE_ASYNC_THROTTLE_MUTEX2; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::Environment; +use test_tools::oauth_request::DEFAULT_CLIENT_CREDENTIALS_MUTEX2; #[tokio::test] async fn download_config_file_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let response = client - .user(id.as_str()) - .drive() - .item_by_path(":/downloadtestdoc.txt:") - .get_items_content() - .send() - .await - .unwrap(); + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX2.lock().await; + let response = test_client + .client + .user(test_client.user_id.as_str()) + .drive() + .item_by_path(":/downloadtestdoc.txt:") + .get_items_content() + .send() + .await + .unwrap(); - let result = response - .download( - &FileConfig::new("./test_files").file_name(OsStr::new("downloadtestdoc.txt")), - ) - .await; + let result = response + .download(&FileConfig::new("./test_files").file_name(OsStr::new("downloadtestdoc.txt"))) + .await; - match result { - Ok(response2) => panic!("Download request should have thrown AsyncDownloadError::FileExists. Instead got successful Response: {:#?}", response2), + match result { + Ok(response2) => panic!("Download request should have thrown AsyncDownloadError::FileExists. Instead got successful Response: {:#?}", response2), - Err(AsyncDownloadError::FileExists(name)) => { - if cfg!(target_os = "windows") { - assert_eq!(name, "./test_files\\downloadtestdoc.txt".to_string()); - } else { - assert_eq!(name, "./test_files/downloadtestdoc.txt".to_string()); - } - } + Err(AsyncDownloadError::FileExists(name)) => { + if cfg!(target_os = "windows") { + assert_eq!(name, "./test_files\\downloadtestdoc.txt".to_string()); + } else { + assert_eq!(name, "./test_files/downloadtestdoc.txt".to_string()); + } + } - Err(err) => panic!("Incorrect error thrown. Should have been AsyncDownloadError::FileExists. Got: {err:#?}"), - } + Err(err) => panic!("Incorrect error thrown. Should have been AsyncDownloadError::FileExists. Got: {err:#?}"), } } } @@ -46,26 +43,25 @@ async fn download_config_file_exists() { #[tokio::test] async fn download_is_err_config_dir_no_exists() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let response = client - .user(id.as_str()) - .drive() - .item_by_path(":/downloadtestdoc.txt:") - .get_items_content() - .send() - .await - .unwrap(); + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX2.lock().await; + let response = test_client + .client + .user(test_client.user_id.as_str()) + .drive() + .item_by_path(":/downloadtestdoc.txt:") + .get_items_content() + .send() + .await + .unwrap(); - let result = response - .download(&FileConfig::new("./test_files/download_dir").create_directories(false)) - .await; + let result = response + .download(&FileConfig::new("./test_files/download_dir").create_directories(false)) + .await; - match result { - Ok(response) => panic!("Download request should have thrown AsyncDownloadError::TargetDoesNotExist. Instead got successful PathBuf: {response:#?}"), - Err(AsyncDownloadError::TargetDoesNotExist(dir)) => assert_eq!("./test_files/download_dir".to_string(), dir), - Err(err) => panic!("Incorrect error thrown. Should have been AsyncDownloadError::TargetDoesNotExist. Got: {err:#?}"), - } + match result { + Ok(response) => panic!("Download request should have thrown AsyncDownloadError::TargetDoesNotExist. Instead got successful PathBuf: {response:#?}"), + Err(AsyncDownloadError::TargetDoesNotExist(dir)) => assert_eq!("./test_files/download_dir".to_string(), dir), + Err(err) => panic!("Incorrect error thrown. Should have been AsyncDownloadError::TargetDoesNotExist. Got: {err:#?}"), } } } diff --git a/tests/drive_download_request.rs b/tests/drive_download_request.rs index 53072707..9c01176c 100644 --- a/tests/drive_download_request.rs +++ b/tests/drive_download_request.rs @@ -2,17 +2,46 @@ use graph_http::traits::ResponseExt; use graph_rs_sdk::http::FileConfig; use graph_rs_sdk::*; use std::ffi::OsStr; -use test_tools::oauth_request::{Environment, OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX2}; +use test_tools::oauth_request::{Environment, DEFAULT_CLIENT_CREDENTIALS_MUTEX4}; use test_tools::support::cleanup::AsyncCleanUp; #[tokio::test] async fn drive_download() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let response = client - .drive(id.as_str()) + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let response = test_client + .client + .drive(test_client.user_id.as_str()) + .item_by_path(":/test_document.docx:") + .get_items_content() + .send() + .await + .unwrap(); + + assert!(response.status().is_success()); + + let response2 = response + .download(&FileConfig::new("./test_files")) + .await + .unwrap(); + + let path_buf = response2.into_body(); + assert!(path_buf.exists()); + + let file_location = "./test_files/test_document.docx"; + let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); + clean_up.rm_files(file_location.into()); +} + +#[tokio::test] +async fn drive_download_format() { + if Environment::is_local() { + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let response = test_client + .client + .drive(test_client.user_id.as_str()) .item_by_path(":/test_document.docx:") .get_items_content() + .format("pdf") .send() .await .unwrap(); @@ -20,50 +49,17 @@ async fn drive_download() { assert!(response.status().is_success()); let response2 = response - .download(&FileConfig::new("./test_files")) + .download(&FileConfig::new("./test_files").file_name(OsStr::new("test_document.pdf"))) .await .unwrap(); let path_buf = response2.into_body(); assert!(path_buf.exists()); + assert_eq!(path_buf.extension(), Some(OsStr::new("pdf"))); + assert_eq!(path_buf.file_name(), Some(OsStr::new("test_document.pdf"))); - let file_location = "./test_files/test_document.docx"; + let file_location = "./test_files/test_document.pdf"; let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); clean_up.rm_files(file_location.into()); } } - -#[tokio::test] -async fn drive_download_format() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX2.lock().await; - if Environment::is_local() { - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let response = client - .drive(id.as_str()) - .item_by_path(":/test_document.docx:") - .get_items_content() - .format("pdf") - .send() - .await - .unwrap(); - - assert!(response.status().is_success()); - - let response2 = response - .download( - &FileConfig::new("./test_files").file_name(OsStr::new("test_document.pdf")), - ) - .await - .unwrap(); - - let path_buf = response2.into_body(); - assert!(path_buf.exists()); - assert_eq!(path_buf.extension(), Some(OsStr::new("pdf"))); - assert_eq!(path_buf.file_name(), Some(OsStr::new("test_document.pdf"))); - - let file_location = "./test_files/test_document.pdf"; - let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); - clean_up.rm_files(file_location.into()); - } - } -} diff --git a/tests/drive_request.rs b/tests/drive_request.rs index 15533267..cf63b015 100644 --- a/tests/drive_request.rs +++ b/tests/drive_request.rs @@ -8,73 +8,75 @@ use std::fs::OpenOptions; use std::io::Write; use std::time::Duration; -use test_tools::oauth_request::DRIVE_ASYNC_THROTTLE_MUTEX; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::{ + Environment, DEFAULT_CLIENT_CREDENTIALS_MUTEX, DEFAULT_CLIENT_CREDENTIALS_MUTEX3, + DEFAULT_CLIENT_CREDENTIALS_MUTEX4, +}; #[tokio::test] async fn list_versions_get_item() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let get_item_res = client - .user(id.as_str()) - .drive() - .item_by_path(":/copy_folder:") - .get_items() - .send() - .await; + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let get_item_res = test_client + .client + .user(test_client.user_id.as_str()) + .drive() + .item_by_path(":/copy_folder:") + .get_items() + .send() + .await; - if let Ok(res) = get_item_res { - let body: serde_json::Value = res.json().await.unwrap(); - assert!(body["id"].as_str().is_some()); - let item_id = body["id"].as_str().unwrap(); + if let Ok(res) = get_item_res { + let body: serde_json::Value = res.json().await.unwrap(); + assert!(body["id"].as_str().is_some()); + let item_id = body["id"].as_str().unwrap(); - let response = client - .user(id.as_str()) - .drive() - .item(item_id) - .list_versions() - .send() - .await - .unwrap(); + let response = test_client + .client + .user(test_client.user_id.as_str()) + .drive() + .item(item_id) + .list_versions() + .send() + .await + .unwrap(); - assert!(response.status().is_success()); - } else if let Err(e) = get_item_res { - panic!("Request Error. Method: drive get_item. Error: {e:#?}"); - } + assert!(response.status().is_success()); + } else if let Err(e) = get_item_res { + panic!("Request Error. Method: drive get_item. Error: {e:#?}"); } } } #[tokio::test] async fn drive_check_in_out() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; if Environment::is_local() { - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .drive(id.as_str()) - .item_by_path(":/test_check_out_document.docx:") - .checkout() - .header(CONTENT_LENGTH, HeaderValue::from(0)) - .send() - .await; + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + let result = test_client + .client + .drive(test_client.user_id.as_str()) + .item_by_path(":/test_check_out_document.docx:") + .checkout() + .header(CONTENT_LENGTH, HeaderValue::from(0)) + .send() + .await; - let response = result.unwrap(); - assert!(response.status().is_success()); - tokio::time::sleep(Duration::from_secs(2)).await; + let response = result.unwrap(); + assert!(response.status().is_success()); + tokio::time::sleep(Duration::from_millis(500)).await; - let response = client - .drive(id.as_str()) - .item_by_path(":/test_check_out_document.docx:") - .checkin(&serde_json::json!({ - "comment": "test check in", - })) - .send() - .await - .unwrap(); + let response = test_client + .client + .drive(test_client.user_id.as_str()) + .item_by_path(":/test_check_out_document.docx:") + .checkin(&serde_json::json!({ + "comment": "test check in", + })) + .send() + .await + .unwrap(); - assert!(response.status().is_success()); - } + assert!(response.status().is_success()); } } @@ -92,48 +94,45 @@ async fn update_item_by_path( .await } +#[ignore] #[tokio::test] async fn drive_update() { if Environment::is_local() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let req = update_item_by_path( + test_client.user_id.as_str(), + ":/update_test_document.docx:", + &serde_json::json!({ + "name": "update_test.docx" + }), + &test_client.client, + ) + .await; + + if let Ok(response) = req { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + assert_eq!(body["name"].as_str(), Some("update_test.docx")); + let req = update_item_by_path( - id.as_str(), - ":/update_test_document.docx:", + test_client.user_id.as_str(), + ":/update_test.docx:", &serde_json::json!({ - "name": "update_test.docx" + "name": "update_test_document.docx" }), - &client, + &test_client.client, ) .await; if let Ok(response) = req { assert!(response.status().is_success()); let body: serde_json::Value = response.json().await.unwrap(); - assert_eq!(body["name"].as_str(), Some("update_test.docx")); - - tokio::time::sleep(Duration::from_secs(2)).await; - - let req = update_item_by_path( - id.as_str(), - ":/update_test.docx:", - &serde_json::json!({ - "name": "update_test_document.docx" - }), - &client, - ) - .await; - - if let Ok(response) = req { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - assert_eq!(body["name"].as_str(), Some("update_test_document.docx")); - } else if let Err(e) = req { - panic!("Request Error. Method: drive update. Error: {e:#?}"); - } + assert_eq!(body["name"].as_str(), Some("update_test_document.docx")); } else if let Err(e) = req { - panic!("Request Error. Method: drive check_out. Error: {e:#?}"); + panic!("Request Error. Method: drive update. Error: {e:#?}"); } + } else if let Err(e) = req { + panic!("Request Error. Method: drive check_out. Error: {e:#?}"); } } } @@ -197,60 +196,67 @@ async fn delete_file( #[tokio::test] async fn drive_upload_item() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let local_file = "./test_files/test_upload_file.txt"; - let file_name = ":/test_upload_file.txt:"; - let onedrive_file_path = ":/Documents/test_upload_file.txt:"; + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX.lock().await; + let local_file = "./test_files/test_upload_file.txt"; + let file_name = ":/test_upload_file.txt:"; + let onedrive_file_path = ":/Documents/test_upload_file.txt:"; - let parent_reference_id = get_special_folder_id(id.as_str(), "Documents", &client) - .await - .unwrap(); - let upload_res = upload_new_file( - id.as_str(), - parent_reference_id.as_str(), - file_name, - local_file, - &client, - ) - .await; + let parent_reference_id = get_special_folder_id( + test_client.user_id.as_str(), + "Documents", + &test_client.client, + ) + .await + .unwrap(); + let upload_res = upload_new_file( + test_client.user_id.as_str(), + parent_reference_id.as_str(), + file_name, + local_file, + &test_client.client, + ) + .await; - if let Ok(response) = upload_res { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - assert!(body["id"].as_str().is_some()); - let item_id = body["id"].as_str().unwrap(); + if let Ok(response) = upload_res { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + assert!(body["id"].as_str().is_some()); + let item_id = body["id"].as_str().unwrap(); - let mut file = OpenOptions::new().write(true).open(local_file).unwrap(); - file.write_all("Test Update File".as_bytes()).unwrap(); - file.sync_all().unwrap(); + let mut file = OpenOptions::new().write(true).open(local_file).unwrap(); + file.write_all("Test Update File".as_bytes()).unwrap(); + file.sync_all().unwrap(); - tokio::time::sleep(Duration::from_secs(2)).await; + tokio::time::sleep(Duration::from_millis(500)).await; - let update_res = - update_file(id.as_str(), onedrive_file_path, local_file, &client).await; + let update_res = update_file( + test_client.user_id.as_str(), + onedrive_file_path, + local_file, + &test_client.client, + ) + .await; - if let Ok(response2) = update_res { - assert!(response2.status().is_success()); - let body: serde_json::Value = response2.json().await.unwrap(); - assert!(body["id"].as_str().is_some()); - let item_id2 = body["id"].as_str().unwrap(); - assert_eq!(item_id, item_id2); - } else if let Err(err) = update_res { - panic!("Request Error. Method: update item. Error: {err:#?}"); - } + if let Ok(response2) = update_res { + assert!(response2.status().is_success()); + let body: serde_json::Value = response2.json().await.unwrap(); + let item_id2 = body["id"].as_str().unwrap(); + assert_eq!(item_id, item_id2); + } else if let Err(err) = update_res { + panic!("Request Error. Method: update item. Error: {err:#?}"); + } - tokio::time::sleep(Duration::from_secs(2)).await; + tokio::time::sleep(Duration::from_millis(500)).await; - let delete_res = delete_file(id.as_str(), item_id, &client).await; + let delete_res = + delete_file(test_client.user_id.as_str(), item_id, &test_client.client).await; - if let Ok(response) = delete_res { - assert!(response.status().is_success()); - } else if let Err(err) = delete_res { - panic!("Request Error. Method: drive delete. Error: {err:#?}"); - } - } else if let Err(err) = upload_res { - panic!("Request Error. Method: drive upload. Error: {err:#?}"); + if let Ok(response) = delete_res { + assert!(response.status().is_success()); + } else if let Err(err) = delete_res { + panic!("Request Error. Method: drive delete. Error: {err:#?}"); } + } else if let Err(err) = upload_res { + panic!("Request Error. Method: drive upload. Error: {err:#?}"); } } diff --git a/tests/drive_url.rs b/tests/drive_url.rs index 22e347b0..e9aa6242 100644 --- a/tests/drive_url.rs +++ b/tests/drive_url.rs @@ -72,7 +72,11 @@ pub fn drive_preview_path() { .url() .path() ); - assert_eq!("/v1.0/users/T5Y6RODPNfYICbtYWrofwUGBJWnaJkNwH9x/drive/root:/Documents/preview.txt:/preview".to_string(), client.user(RID).drive().item_by_path(":/Documents/preview.txt:").preview(&serde_json::json!({})).url().path()); + assert_eq!("/v1.0/users/T5Y6RODPNfYICbtYWrofwUGBJWnaJkNwH9x/drive/root:/Documents/preview.txt:/preview" + .to_string(), + client.user(RID).drive().item_by_path(":/Documents/preview.txt:") + .preview(&serde_json::json!({})).url().path() + ); } #[test] diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index cd3d3a34..b954f794 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -1,52 +1,41 @@ -use graph_core::resource::ResourceIdentity; use graph_http::api_impl::ODataQuery; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX2; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::{DEFAULT_ONENOTE_CREDENTIALS_MUTEX}; +#[ignore] #[tokio::test] async fn get_drafts_mail_folder() { - if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = - OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await - { - let response = client - .user(id.as_str()) - .mail_folder("drafts") - .get_mail_folders() - .send() - .await - .unwrap(); + let test_client = DEFAULT_ONENOTE_CREDENTIALS_MUTEX.lock().await; + let response = test_client + .client + .user(test_client.user_id.as_str()) + .mail_folder("drafts") + .get_mail_folders() + .send() + .await + .unwrap(); - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - let display_name = body["displayName"].as_str().unwrap(); - assert_eq!("Drafts", display_name); - } - } + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + let display_name = body["displayName"].as_str().unwrap(); + assert_eq!("Drafts", display_name); } #[tokio::test] async fn mail_folder_list_messages() { - if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = - OAuthTestClient::graph_by_rid_async(ResourceIdentity::MailFolders).await - { - let response = client - .user(id.as_str()) - .mail_folder("inbox") - .messages() - .list_messages() - .top("2") - .send() - .await - .unwrap(); + let test_client = DEFAULT_ONENOTE_CREDENTIALS_MUTEX.lock().await; + let response = test_client + .client + .user(test_client.user_id.as_str()) + .mail_folder("inbox") + .messages() + .list_messages() + .top("2") + .send() + .await + .unwrap(); - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - let messages = body["value"].as_array().unwrap(); - assert_eq!(messages.len(), 2); - } - } + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + let messages = body["value"].as_array().unwrap(); + assert_eq!(messages.len(), 2); } diff --git a/tests/message_request.rs b/tests/message_request.rs index b3a588d7..6756ba2d 100644 --- a/tests/message_request.rs +++ b/tests/message_request.rs @@ -1,39 +1,37 @@ -use std::thread; use std::time::Duration; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX2; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::{Environment, DEFAULT_CLIENT_CREDENTIALS_MUTEX3}; #[tokio::test] async fn list_and_get_messages() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - if let Ok(response) = client - .user(id.as_str()) - .messages() - .list_messages() + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + if let Ok(response) = test_client + .client + .user(test_client.user_id.as_str()) + .messages() + .list_messages() + .send() + .await + { + let body: serde_json::Value = response.json().await.unwrap(); + let vec = body["value"].as_array().unwrap(); + let message_id = vec[0]["id"].as_str().unwrap(); + + let response = test_client + .client + .user(test_client.user_id.as_str()) + .message(message_id) + .get_messages() .send() .await - { - let body: serde_json::Value = response.json().await.unwrap(); - let vec = body["value"].as_array().unwrap(); - let message_id = vec[0]["id"].as_str().unwrap(); - - let response = client - .user(id.as_str()) - .message(message_id) - .get_messages() - .send() - .await - .unwrap(); + .unwrap(); - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - let m_id = body["id"].as_str().unwrap(); - assert_eq!(m_id, message_id); - } else { - panic!("Request error. Method: mail messages list"); - } + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + let m_id = body["id"].as_str().unwrap(); + assert_eq!(m_id, message_id); + } else { + panic!("Request error. Method: mail messages list"); } } } @@ -41,48 +39,49 @@ async fn list_and_get_messages() { #[tokio::test] async fn mail_create_and_delete_message() { if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX2.lock().await; - if let Some((id, mut client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client - .v1() - .user(id.as_str()) - .messages() - .create_messages(&serde_json::json!({ - "subject":"Did you see last night's game?", - "importance":"Low", - "body":{ - "contentType":"HTML", - "content":"They were <b>awesome</b>!" - }, - "toRecipients":[ - { - "emailAddress":{ - "address":"AdeleV@contoso.onmicrosoft.com" - } + let mut test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + let user_id = test_client.user_id.clone(); + let result = test_client + .client + .v1() + .user(user_id.as_str()) + .messages() + .create_messages(&serde_json::json!({ + "subject":"Did you see last night's game?", + "importance":"Low", + "body":{ + "contentType":"HTML", + "content":"They were <b>awesome</b>!" + }, + "toRecipients":[ + { + "emailAddress":{ + "address":"AdeleV@contoso.onmicrosoft.com" } - ] - })) - .send() - .await; + } + ] + })) + .send() + .await; - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let message_id = body["id"].as_str().unwrap(); + if let Ok(response) = result { + let body: serde_json::Value = response.json().await.unwrap(); + let message_id = body["id"].as_str().unwrap(); - thread::sleep(Duration::from_secs(2)); - let delete_res = client - .v1() - .user(id.as_str()) - .message(message_id) - .delete_messages() - .send() - .await; - if let Err(e) = delete_res { - panic!("Request error. Method: mail messages delete. Error: {e:#?}"); - } - } else if let Err(e) = result { - panic!("Request error. Method: mail messages create. Error: {e:#?}"); + tokio::time::sleep(Duration::from_secs(1)).await; + let delete_res = test_client + .client + .v1() + .user(user_id.as_str()) + .message(message_id) + .delete_messages() + .send() + .await; + if let Err(e) = delete_res { + panic!("Request error. Method: mail messages delete. Error: {e:#?}"); } + } else if let Err(e) = result { + panic!("Request error. Method: mail messages create. Error: {e:#?}"); } } } diff --git a/tests/onenote_request.rs b/tests/onenote_request.rs index 962190b7..069bbea2 100644 --- a/tests/onenote_request.rs +++ b/tests/onenote_request.rs @@ -1,12 +1,11 @@ -use graph_core::resource::ResourceIdentity; use graph_http::traits::ResponseExt; use graph_rs_sdk::header::{HeaderValue, CONTENT_TYPE}; use graph_rs_sdk::http::FileConfig; use std::ffi::OsStr; use std::thread; use std::time::Duration; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::Environment; +use test_tools::oauth_request::DEFAULT_ONENOTE_CREDENTIALS_MUTEX; use test_tools::support::cleanup::AsyncCleanUp; #[tokio::test] @@ -15,66 +14,66 @@ async fn list_get_notebooks_and_sections() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await - { - let notebooks = client - .user(id.as_str()) + let test_client = DEFAULT_ONENOTE_CREDENTIALS_MUTEX.lock().await; + let notebooks = test_client + .client + .user(test_client.user_id.as_str()) + .onenote() + .notebooks() + .list_notebooks() + .send() + .await; + + if let Ok(response) = notebooks { + let body: serde_json::Value = response.json().await.unwrap(); + let vec = body["value"].as_array().unwrap(); + + let mut found_test_notebook = false; + let mut notebook_id = String::new(); + for value in vec.iter() { + if value["displayName"].as_str().unwrap().eq("TestNotebook") { + found_test_notebook = true; + notebook_id.push_str(value["id"].as_str().unwrap()); + } + } + + assert!(found_test_notebook); + let get_notebook = test_client + .client + .user(test_client.user_id.as_str()) .onenote() - .notebooks() - .list_notebooks() + .notebook(notebook_id.as_str()) + .get_notebooks() .send() .await; - if let Ok(response) = notebooks { - let body: serde_json::Value = response.json().await.unwrap(); - let vec = body["value"].as_array().unwrap(); - - let mut found_test_notebook = false; - let mut notebook_id = String::new(); - for value in vec.iter() { - if value["displayName"].as_str().unwrap().eq("TestNotebook") { - found_test_notebook = true; - notebook_id.push_str(value["id"].as_str().unwrap()); - } - } + if let Ok(notebook_response) = get_notebook { + let body: serde_json::Value = notebook_response.json().await.unwrap(); + assert_eq!("TestNotebook", body["displayName"].as_str().unwrap()); + } else if let Err(e) = get_notebook { + panic!("Request error. Method: onenote notebooks get. Error: {e:#?}"); + } - assert!(found_test_notebook); - let get_notebook = client - .user(id.as_str()) - .onenote() - .notebook(notebook_id.as_str()) - .get_notebooks() - .send() - .await; - - if let Ok(notebook_response) = get_notebook { - let body: serde_json::Value = notebook_response.json().await.unwrap(); - assert_eq!("TestNotebook", body["displayName"].as_str().unwrap()); - } else if let Err(e) = get_notebook { - panic!("Request error. Method: onenote notebooks get. Error: {e:#?}"); - } + let result = test_client + .client + .user(test_client.user_id.as_str()) + .onenote() + .notebook(notebook_id.as_str()) + .sections() + .list_sections() + .send() + .await; - let result = client - .user(id.as_str()) - .onenote() - .notebook(notebook_id.as_str()) - .sections() - .list_sections() - .send() - .await; - - if let Ok(response) = result { - let body: serde_json::Value = response.json().await.unwrap(); - let vec = body["value"].as_array().unwrap(); - let section_name = vec[0]["displayName"].as_str().unwrap(); - assert_eq!("TestSection", section_name); - } else if let Err(e) = result { - panic!("Request error. Method: onenote notebooks list sections. Error: {e:#?}"); - } - } else if let Err(e) = notebooks { - panic!("Request error. Method: onenote notebooks list. Error: {e:#?}"); + if let Ok(response) = result { + let body: serde_json::Value = response.json().await.unwrap(); + let vec = body["value"].as_array().unwrap(); + let section_name = vec[0]["displayName"].as_str().unwrap(); + assert_eq!("TestSection", section_name); + } else if let Err(e) = result { + panic!("Request error. Method: onenote notebooks list sections. Error: {e:#?}"); } + } else if let Err(e) = notebooks { + panic!("Request error. Method: onenote notebooks list. Error: {e:#?}"); } } @@ -84,37 +83,36 @@ async fn create_delete_page_from_file() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await - { - let res = client - .user(&id) + let test_client = DEFAULT_ONENOTE_CREDENTIALS_MUTEX.lock().await; + let res = test_client + .client + .user(&test_client.user_id) + .onenote() + .pages() + .create_pages(&FileConfig::new("./test_files/one_note_page.html")) + .header(CONTENT_TYPE, HeaderValue::from_static("text/html")) + .send() + .await; + + if let Ok(response) = res { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + let page_id = body["id"].as_str().unwrap(); + + thread::sleep(Duration::from_secs(3)); + let delete_res = test_client + .client + .user(&test_client.user_id) .onenote() - .pages() - .create_pages(&FileConfig::new("./test_files/one_note_page.html")) - .header(CONTENT_TYPE, HeaderValue::from_static("text/html")) + .page(page_id) + .delete_pages() .send() - .await; + .await + .unwrap(); - if let Ok(response) = res { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - let page_id = body["id"].as_str().unwrap(); - - thread::sleep(Duration::from_secs(3)); - let delete_res = client - .user(&id) - .onenote() - .page(page_id) - .delete_pages() - .send() - .await - .unwrap(); - - assert!(delete_res.status().is_success()); - } else if let Err(e) = res { - panic!("Request error. Method onenote create page. Error: {e:#?}"); - } + assert!(delete_res.status().is_success()); + } else if let Err(e) = res { + panic!("Request error. Method onenote create page. Error: {e:#?}"); } } @@ -124,61 +122,60 @@ async fn download_page() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((user_id, client)) = - OAuthTestClient::graph_by_rid_async(ResourceIdentity::Onenote).await - { - let file_location = "./test_files/downloaded_page.html"; - let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); - clean_up.rm_files(file_location.into()); - - let res = client - .user(&user_id) + let test_client = DEFAULT_ONENOTE_CREDENTIALS_MUTEX.lock().await; + let file_location = "./test_files/downloaded_page.html"; + let mut clean_up = AsyncCleanUp::new_remove_existing(file_location); + clean_up.rm_files(file_location.into()); + + let res = test_client + .client + .user(&test_client.user_id) + .onenote() + .pages() + .create_pages(&FileConfig::new("./test_files/one_note_page.html")) + .header(CONTENT_TYPE, HeaderValue::from_static("text/html")) + .send() + .await; + + if let Ok(response) = res { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + let page_id = body["id"].as_str().unwrap(); + + thread::sleep(Duration::from_secs(3)); + let response = test_client + .client + .user(&test_client.user_id) .onenote() - .pages() - .create_pages(&FileConfig::new("./test_files/one_note_page.html")) - .header(CONTENT_TYPE, HeaderValue::from_static("text/html")) + .page(page_id) + .get_pages_content() .send() - .await; + .await + .unwrap(); + + let response2 = response + .download( + &FileConfig::new("./test_files").file_name(OsStr::new("downloaded_page.html")), + ) + .await + .unwrap(); + + assert!(response2.status().is_success()); + let path_buf = response2.into_body(); + assert!(path_buf.exists()); + + let response = test_client + .client + .user(&test_client.user_id) + .onenote() + .page(page_id) + .delete_pages() + .send() + .await + .expect("onenote delete pages from page id"); - if let Ok(response) = res { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - let page_id = body["id"].as_str().unwrap(); - - thread::sleep(Duration::from_secs(3)); - let response = client - .user(&user_id) - .onenote() - .page(page_id) - .get_pages_content() - .send() - .await - .unwrap(); - - let response2 = response - .download( - &FileConfig::new("./test_files").file_name(OsStr::new("downloaded_page.html")), - ) - .await - .unwrap(); - - assert!(response2.status().is_success()); - let path_buf = response2.into_body(); - assert!(path_buf.exists()); - - let response = client - .user(&user_id) - .onenote() - .page(page_id) - .delete_pages() - .send() - .await - .expect("onenote delete pages from page id"); - - assert!(response.status().is_success()); - } else if let Err(e) = res { - panic!("Request error. Method onenote create page (download page test) | 01 get content -> download page. Error: {e:#?}"); - } + assert!(response.status().is_success()); + } else if let Err(e) = res { + panic!("Request error. Method onenote create page (download page test) | 01 get content -> download page. Error: {e:#?}"); } } diff --git a/tests/paging.rs b/tests/paging.rs index 4f616af4..02588ed8 100644 --- a/tests/paging.rs +++ b/tests/paging.rs @@ -1,7 +1,6 @@ use futures::StreamExt; use std::collections::VecDeque; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; -use test_tools::oauth_request::{Environment, OAuthTestClient}; +use test_tools::oauth_request::{Environment, DEFAULT_CLIENT_CREDENTIALS_MUTEX4}; #[tokio::test] async fn paging_all() { @@ -9,59 +8,57 @@ async fn paging_all() { return; } - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let mut vec = client - .users() - .delta() - .paging() - .json::<serde_json::Value>() - .await - .unwrap(); + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let mut vec = test_client + .client + .users() + .delta() + .paging() + .json::<serde_json::Value>() + .await + .unwrap(); - assert!(!vec.is_empty()); - for response in vec.iter() { - assert!(response.status().is_success()) - } - - let response = vec.pop_back().unwrap(); - let body = response.into_body().unwrap(); - assert!(body["@odata.deltaLink"].as_str().is_some()) + assert!(!vec.is_empty()); + for response in vec.iter() { + assert!(response.status().is_success()) } + + let response = vec.pop_back().unwrap(); + let body = response.into_body().unwrap(); + assert!(body["@odata.deltaLink"].as_str().is_some()) } #[tokio::test] async fn paging_stream() { if Environment::is_local() { - let _lock = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let mut stream = client - .users() - .delta() - .paging() - .stream::<serde_json::Value>() - .unwrap(); + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX4.lock().await; + let mut stream = test_client + .client + .users() + .delta() + .paging() + .stream::<serde_json::Value>() + .unwrap(); - let mut deque = VecDeque::new(); - while let Some(result) = stream.next().await { - match result { - Ok(response) => { - assert!(response.status().is_success()); - let body = response.into_body().unwrap(); - deque.push_back(body); - } - Err(err) => panic!("Error on stream users delta\n{err:#?}"), + let mut deque = VecDeque::new(); + while let Some(result) = stream.next().await { + match result { + Ok(response) => { + assert!(response.status().is_success()); + let body = response.into_body().unwrap(); + deque.push_back(body); } + Err(err) => panic!("Error on stream users delta\n{err:#?}"), } + } - assert!(deque.len() >= 2); - let last = deque.pop_back().unwrap(); - assert!(last["@odata.deltaLink"].as_str().is_some()); + assert!(deque.len() >= 2); + let last = deque.pop_back().unwrap(); + assert!(last["@odata.deltaLink"].as_str().is_some()); - for body in deque.iter() { - assert!(body["@odata.nextLink"].as_str().is_some()); - assert!(body["@odata.deltaLink"].as_str().is_none()); - } + for body in deque.iter() { + assert!(body["@odata.nextLink"].as_str().is_some()); + assert!(body["@odata.deltaLink"].as_str().is_none()); } } } diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs index c3888280..80ccc53f 100644 --- a/tests/todo_tasks_request.rs +++ b/tests/todo_tasks_request.rs @@ -18,7 +18,7 @@ struct TodoListsTasks { } #[tokio::test] -async fn list_users() { +async fn list_todo_list_tasks() { let _ = ASYNC_THROTTLE_MUTEX.lock().await; if let Some((id, client)) = OAuthTestClient::graph_by_rid_async(ResourceIdentity::TodoListsTasks).await diff --git a/tests/upload_request.rs b/tests/upload_request.rs index c50c7fe3..94f515ea 100644 --- a/tests/upload_request.rs +++ b/tests/upload_request.rs @@ -1,10 +1,8 @@ use bytes::BytesMut; use graph_rs_sdk::http::{BodyRead, FileConfig}; use graph_rs_sdk::*; -use std::thread; use std::time::Duration; - -use test_tools::oauth_request::{OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX}; +use test_tools::oauth_request::DEFAULT_CLIENT_CREDENTIALS_MUTEX3; async fn get_special_folder_id(user_id: &str, folder: &str, client: &Graph) -> GraphResult<String> { let response = client @@ -86,96 +84,102 @@ async fn get_file_content( #[tokio::test] async fn upload_bytes_mut() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let local_file = "./test_files/test_upload_file_bytes.txt"; - let file_name = ":/test_upload_file_bytes.txt:"; - - let parent_reference_id = get_special_folder_id(id.as_str(), "Documents", &client) + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + let local_file = "./test_files/test_upload_file_bytes.txt"; + let file_name = ":/test_upload_file_bytes.txt:"; + + let parent_reference_id = get_special_folder_id( + test_client.user_id.as_str(), + "Documents", + &test_client.client, + ) + .await + .unwrap(); + let upload_res = upload_new_file( + test_client.user_id.as_str(), + parent_reference_id.as_str(), + file_name, + local_file, + &test_client.client, + ) + .await; + + if let Ok(response) = upload_res { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + assert!(body["id"].as_str().is_some()); + let item_id = body["id"].as_str().unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let response = get_file_content(test_client.user_id.as_str(), item_id, &test_client.client) .await .unwrap(); - let upload_res = upload_new_file( - id.as_str(), - parent_reference_id.as_str(), - file_name, - local_file, - &client, - ) - .await; - - if let Ok(response) = upload_res { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - assert!(body["id"].as_str().is_some()); - let item_id = body["id"].as_str().unwrap(); + assert!(response.status().is_success()); - thread::sleep(Duration::from_secs(2)); + let text = response.text().await.unwrap(); + assert_eq!("Upload Bytes", text.trim()); - let response = get_file_content(id.as_str(), item_id, &client) - .await - .unwrap(); - assert!(response.status().is_success()); + let delete_res = + delete_file(test_client.user_id.as_str(), item_id, &test_client.client).await; - let text = response.text().await.unwrap(); - assert_eq!("Upload Bytes", text.trim()); - - let delete_res = delete_file(id.as_str(), item_id, &client).await; - - if let Ok(response) = delete_res { - assert!(response.status().is_success()); - } else if let Err(err) = delete_res { - panic!("Request Error. Method: drive delete. Error: {err:#?}"); - } - } else if let Err(err) = upload_res { - panic!("Request Error. Method: drive upload. Error: {err:#?}"); + if let Ok(response) = delete_res { + assert!(response.status().is_success()); + } else if let Err(err) = delete_res { + panic!("Request Error. Method: drive delete. Error: {err:#?}"); } + } else if let Err(err) = upload_res { + panic!("Request Error. Method: drive upload. Error: {err:#?}"); } } #[tokio::test] async fn upload_reqwest_body() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let local_file = "./test_files/test_upload_file_bytes.txt"; - let file_name = ":/test_upload_file_bytes.txt:"; - - let parent_reference_id = get_special_folder_id(id.as_str(), "Documents", &client) + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX3.lock().await; + let local_file = "./test_files/test_upload_file_bytes.txt"; + let file_name = ":/test_upload_file_bytes.txt:"; + + let parent_reference_id = get_special_folder_id( + test_client.user_id.as_str(), + "Documents", + &test_client.client, + ) + .await + .unwrap(); + let upload_res = upload_file_reqwest_body( + test_client.user_id.as_str(), + parent_reference_id.as_str(), + file_name, + local_file, + &test_client.client, + ) + .await; + + if let Ok(response) = upload_res { + assert!(response.status().is_success()); + let body: serde_json::Value = response.json().await.unwrap(); + assert!(body["id"].as_str().is_some()); + let item_id = body["id"].as_str().unwrap(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let response = get_file_content(test_client.user_id.as_str(), item_id, &test_client.client) .await .unwrap(); - let upload_res = upload_file_reqwest_body( - id.as_str(), - parent_reference_id.as_str(), - file_name, - local_file, - &client, - ) - .await; - - if let Ok(response) = upload_res { - assert!(response.status().is_success()); - let body: serde_json::Value = response.json().await.unwrap(); - assert!(body["id"].as_str().is_some()); - let item_id = body["id"].as_str().unwrap(); - - thread::sleep(Duration::from_secs(2)); + assert!(response.status().is_success()); - let response = get_file_content(id.as_str(), item_id, &client) - .await - .unwrap(); - assert!(response.status().is_success()); - - let text = response.text().await.unwrap(); - assert_eq!("Upload Bytes", text.trim()); + let text = response.text().await.unwrap(); + assert_eq!("Upload Bytes", text.trim()); - let delete_res = delete_file(id.as_str(), item_id, &client).await; + let delete_res = + delete_file(test_client.user_id.as_str(), item_id, &test_client.client).await; - if let Ok(response) = delete_res { - assert!(response.status().is_success()); - } else if let Err(err) = delete_res { - panic!("Request Error. Method: drive delete. Error: {err:#?}"); - } - } else if let Err(err) = upload_res { - panic!("Request Error. Method: drive upload. Error: {err:#?}"); + if let Ok(response) = delete_res { + assert!(response.status().is_success()); + } else if let Err(err) = delete_res { + panic!("Request Error. Method: drive delete. Error: {err:#?}"); } + } else if let Err(err) = upload_res { + panic!("Request Error. Method: drive upload. Error: {err:#?}"); } } diff --git a/tests/upload_request_blocking.rs b/tests/upload_request_blocking.rs index 034e2fdb..038cf0f3 100644 --- a/tests/upload_request_blocking.rs +++ b/tests/upload_request_blocking.rs @@ -83,7 +83,7 @@ fn upload_reqwest_body() { assert!(body["id"].as_str().is_some()); let item_id = body["id"].as_str().unwrap(); - thread::sleep(Duration::from_secs(5)); + thread::sleep(Duration::from_secs(3)); let response = get_file_content(id.as_str(), item_id, &client).unwrap(); assert!(response.status().is_success()); diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index f1744249..22750a86 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -5,7 +5,7 @@ use graph_http::traits::ResponseExt; use graph_rs_sdk::Graph; use std::time::Duration; -use test_tools::oauth_request::{OAuthTestClient, DRIVE_ASYNC_THROTTLE_MUTEX}; +use test_tools::oauth_request::DEFAULT_CLIENT_CREDENTIALS_MUTEX2; async fn delete_item( drive_id: &str, @@ -162,31 +162,45 @@ async fn file_upload_session_channel( // This is a long running test. 20 - 30 seconds. #[tokio::test] async fn test_upload_session() { - let _lock = DRIVE_ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let item_by_path = ":/upload_session_file.txt:"; - let local_file = "./test_files/upload_session_file.txt"; - - // Stream Upload Session - let stream_item_id = - file_upload_session_stream(id.as_str(), item_by_path, local_file, &client) - .await - .unwrap(); - let response = delete_item(id.as_str(), stream_item_id.as_str(), &client) - .await - .unwrap(); - assert!(response.status().is_success()); - - tokio::time::sleep(Duration::from_secs(2)).await; - - // Channel Upload Session - let channel_item_id = - file_upload_session_channel(id.as_str(), item_by_path, local_file, &client) - .await - .unwrap(); - let response = delete_item(id.as_str(), channel_item_id.as_str(), &client) - .await - .unwrap(); - assert!(response.status().is_success()); - } + let test_client = DEFAULT_CLIENT_CREDENTIALS_MUTEX2.lock().await; + let item_by_path = ":/upload_session_file.txt:"; + let local_file = "./test_files/upload_session_file.txt"; + + // Stream Upload Session + let stream_item_id = file_upload_session_stream( + test_client.user_id.as_str(), + item_by_path, + local_file, + &test_client.client, + ) + .await + .unwrap(); + let response = delete_item( + test_client.user_id.as_str(), + stream_item_id.as_str(), + &test_client.client, + ) + .await + .unwrap(); + assert!(response.status().is_success()); + + tokio::time::sleep(Duration::from_secs(2)).await; + + // Channel Upload Session + let channel_item_id = file_upload_session_channel( + test_client.user_id.as_str(), + item_by_path, + local_file, + &test_client.client, + ) + .await + .unwrap(); + let response = delete_item( + test_client.user_id.as_str(), + channel_item_id.as_str(), + &test_client.client, + ) + .await + .unwrap(); + assert!(response.status().is_success()); } diff --git a/tests/user_request.rs b/tests/user_request.rs deleted file mode 100644 index e6c37873..00000000 --- a/tests/user_request.rs +++ /dev/null @@ -1,41 +0,0 @@ -use graph_rs_sdk::http::ODataMetadataLink; -use test_tools::oauth_request::ASYNC_THROTTLE_MUTEX; -use test_tools::oauth_request::{Environment, OAuthTestClient}; - -#[tokio::test] -async fn list_users() { - if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((_id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client.users().list_user().send().await; - - if let Ok(response) = result { - assert!(response.status().is_success()); - let value = response.json::<serde_json::Value>().await.unwrap(); - let metadata_link = value.odata_metadata_link().unwrap(); - assert_eq!( - "https://graph.microsoft.com/v1.0/$metadata#users", - metadata_link.as_str() - ); - } else if let Err(e) = result { - panic!("Request error. Method: users list. Error: {e:#?}"); - } - } - } -} - -#[tokio::test] -async fn get_user() { - if Environment::is_local() { - let _ = ASYNC_THROTTLE_MUTEX.lock().await; - if let Some((id, client)) = OAuthTestClient::ClientCredentials.graph_async().await { - let result = client.users().id(id).get_user().send().await; - - if let Ok(response) = result { - assert!(response.status().is_success()); - } else if let Err(e) = result { - panic!("Request error. Method: users list. Error: {e:#?}"); - } - } - } -} From 3967941f3161b4a61f84edba0e04975f7f6a113e Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:30:30 -0500 Subject: [PATCH 070/118] Refactor interactive auth for linux/macos to prevent failures based on os specific traits --- Cargo.toml | 6 +- .../README.md | 18 +- .../auth_code.rs | 0 .../main.rs | 4 +- .../openid.rs | 16 +- .../webview_errors.rs | 0 .../webview_options.rs | 21 ++ .../client_credentials_certificate.rs | 2 +- graph-http/src/client.rs | 14 +- graph-oauth/README.md | 2 +- .../src/identity/allowed_host_validator.rs | 53 ++- .../identity/authorization_query_response.rs | 19 ++ .../src/identity/credentials/app_config.rs | 16 +- .../auth_code_authorization_url.rs | 5 +- ...thorization_code_certificate_credential.rs | 48 ++- .../credentials/device_code_credential.rs | 4 +- .../credentials/open_id_authorization_url.rs | 6 +- .../identity/device_authorization_response.rs | 2 +- .../src/web/interactive_authenticator.rs | 68 ++-- graph-oauth/src/web/interactive_web_view.rs | 312 ------------------ graph-oauth/src/web/mod.rs | 7 +- graph-oauth/src/web/webview_host_validator.rs | 89 +++++ ...web_view_options.rs => webview_options.rs} | 24 +- src/client/graph.rs | 27 +- tests/mail_folder_request.rs | 2 +- 25 files changed, 321 insertions(+), 444 deletions(-) rename examples/{interactive_authentication => interactive_auth}/README.md (88%) rename examples/{interactive_authentication => interactive_auth}/auth_code.rs (100%) rename examples/{interactive_authentication => interactive_auth}/main.rs (68%) rename examples/{interactive_authentication => interactive_auth}/openid.rs (60%) rename examples/{interactive_authentication => interactive_auth}/webview_errors.rs (100%) rename examples/{interactive_authentication => interactive_auth}/webview_options.rs (64%) delete mode 100644 graph-oauth/src/web/interactive_web_view.rs create mode 100644 graph-oauth/src/web/webview_host_validator.rs rename graph-oauth/src/web/{web_view_options.rs => webview_options.rs} (88%) diff --git a/Cargo.toml b/Cargo.toml index 975b9162..a310d114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,11 +77,11 @@ test-tools = { path = "./test-tools", version = "0.0.1" } debug = false [[example]] -name = "oauth_certificate_main" +name = "oauth_certificate" path = "examples/oauth_certificate/main.rs" required-features = ["openssl"] [[example]] -name = "interactive_auth_main" -path = "examples/interactive_authentication/main.rs" +name = "interactive_auth" +path = "examples/interactive_auth/main.rs" required-features = ["interactive-auth"] diff --git a/examples/interactive_authentication/README.md b/examples/interactive_auth/README.md similarity index 88% rename from examples/interactive_authentication/README.md rename to examples/interactive_auth/README.md index f1c28580..e9485616 100644 --- a/examples/interactive_authentication/README.md +++ b/examples/interactive_auth/README.md @@ -34,33 +34,21 @@ static USER_ID: &str = "USER_ID"; static REDIRECT_URI: &str = "http://localhost:8000/redirect"; async fn authenticate() { - // Create a tracing subscriber to log debug/trace events coming from - // authorization http calls and the Graph client. - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - .with_max_level(tracing::Level::TRACE) - .init(); + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT_ID) .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(REDIRECT_URI) - .with_interactive_authentication(None) + .with_interactive_authentication(Default::default()) .unwrap(); let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); let client = GraphClient::from(&confidential_client); - - let response = client.user(USER_ID).get_user().send().await.unwrap(); - - println!("{response:#?}"); - let body: serde_json::Value = response.json().await.unwrap(); - println!("{body:#?}"); } - ``` diff --git a/examples/interactive_authentication/auth_code.rs b/examples/interactive_auth/auth_code.rs similarity index 100% rename from examples/interactive_authentication/auth_code.rs rename to examples/interactive_auth/auth_code.rs diff --git a/examples/interactive_authentication/main.rs b/examples/interactive_auth/main.rs similarity index 68% rename from examples/interactive_authentication/main.rs rename to examples/interactive_auth/main.rs index 7139fc4d..6fff785a 100644 --- a/examples/interactive_authentication/main.rs +++ b/examples/interactive_auth/main.rs @@ -1,5 +1,6 @@ #![allow(dead_code, unused, unused_imports)] +extern crate pretty_env_logger; #[macro_use] extern crate log; mod auth_code; @@ -7,4 +8,5 @@ mod openid; mod webview_errors; mod webview_options; -fn main() {} +#[tokio::main] +async fn main() {} diff --git a/examples/interactive_authentication/openid.rs b/examples/interactive_auth/openid.rs similarity index 60% rename from examples/interactive_authentication/openid.rs rename to examples/interactive_auth/openid.rs index 0c0224c4..ae3dda63 100644 --- a/examples/interactive_authentication/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -8,14 +8,14 @@ async fn openid_authenticate( client_id: &str, client_secret: &str, redirect_uri: &str, -) -> anyhow::Result<()> { +) -> anyhow::Result<GraphClient> { std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); let (authorization_query_response, mut credential_builder) = OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) - .with_scope(vec!["user.read", "offline_access"]) // Adds offline_access as a scope which is needed to get a refresh token. + .with_scope(vec!["user.read", "offline_access", "profile", "email"]) // Adds offline_access as a scope which is needed to get a refresh token. .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_redirect_uri(redirect_uri)? @@ -23,15 +23,7 @@ async fn openid_authenticate( debug!("{authorization_query_response:#?}"); - let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + let confidential_client = credential_builder.with_client_secret(client_secret).build(); - let client = GraphClient::from(&confidential_client); - - let response = client.users().list_user().send().await?; - - debug!("{response:#?}"); - let body: serde_json::Value = response.json().await?; - debug!("{body:#?}"); - - Ok(()) + Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/interactive_authentication/webview_errors.rs b/examples/interactive_auth/webview_errors.rs similarity index 100% rename from examples/interactive_authentication/webview_errors.rs rename to examples/interactive_auth/webview_errors.rs diff --git a/examples/interactive_authentication/webview_options.rs b/examples/interactive_auth/webview_options.rs similarity index 64% rename from examples/interactive_authentication/webview_options.rs rename to examples/interactive_auth/webview_options.rs index 49c1eef7..5eeb59bc 100644 --- a/examples/interactive_authentication/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -4,6 +4,7 @@ use std::collections::HashSet; use std::ops::Add; use std::time::{Duration, Instant}; +#[cfg(windows)] fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" @@ -26,6 +27,26 @@ fn get_webview_options() -> WebViewOptions { .with_ports(HashSet::from([8000])) } +#[cfg(unix)] +fn get_webview_options() -> WebViewOptions { + WebViewOptions::builder() + // Give the window a title. The default is "Sign In" + .with_window_title("Sign In") + // Add a timeout that will close the window and return an error + // when that timeout is reached. For instance, if your app is waiting on the + // user to log in and the user has not logged in after 20 minutes you may + // want to assume the user is idle in some way and close out of the webview window. + .with_timeout(Instant::now().add(Duration::from_secs(1200))) + // The webview can store the cookies that were set after sign in so that on the next + // sign in the user is automatically logged in through SSO. Or you can clear the browsing + // data, cookies in this case, after sign in when the webview window closes. + .with_clear_browsing_data(false) + // Provide a list of ports to use for interactive authentication. + // This assumes that you have http://localhost or http://localhost:port + // for each port registered in your ADF application registration. + .with_ports(HashSet::from([8000])) +} + async fn customize_webview( tenant_id: &str, client_id: &str, diff --git a/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs b/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs index b1369494..26fef7f5 100644 --- a/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs +++ b/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs @@ -12,7 +12,7 @@ pub fn x509_certificate( public_key_path: impl AsRef<Path>, private_key_path: impl AsRef<Path>, ) -> anyhow::Result<X509Certificate> { - // Use include_bytes!(file_path) if the files are local + // You can use include_bytes!(file_path) if the files are local let mut cert_file = File::open(public_key_path)?; let mut certificate: Vec<u8> = Vec::new(); cert_file.read_to_end(&mut certificate)?; diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index d57bcc4b..d500d569 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -22,6 +22,8 @@ struct ClientConfiguration { connect_timeout: Option<Duration>, connection_verbose: bool, https_only: bool, + /// TLS 1.2 required to support all features in Microsoft Graph + /// See [Reliability and Support](https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support) min_tls_version: Version, } @@ -42,8 +44,6 @@ impl ClientConfiguration { connect_timeout: None, connection_verbose: false, https_only: true, - /// TLS 1.2 required to support all features in Microsoft Graph - /// See [Reliability and Support](https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support) min_tls_version: Version::TLS_1_2, } } @@ -139,12 +139,14 @@ impl GraphClientConfiguration { self } + /// TLS 1.2 required to support all features in Microsoft Graph + /// See [Reliability and Support](https://learn.microsoft.com/en-us/graph/best-practices-concept#reliability-and-support) pub fn min_tls_version(mut self, version: Version) -> GraphClientConfiguration { self.config.min_tls_version = version; self } - pub fn build(self) -> Client { + pub(crate) fn build(self) -> Client { let config = self.clone(); let headers = self.config.headers.clone(); let mut builder = reqwest::ClientBuilder::new() @@ -279,6 +281,12 @@ impl Debug for Client { } } +impl From<GraphClientConfiguration> for Client { + fn from(value: GraphClientConfiguration) -> Self { + value.build() + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/README.md b/graph-oauth/README.md index aef99054..74060dd0 100644 --- a/graph-oauth/README.md +++ b/graph-oauth/README.md @@ -8,7 +8,7 @@ Support for: - Device Code Polling - Authorization Using Certificates | features = [`openssl`] -Purpose built as OAuth client for Microsoft Graph and the [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) project. +OAuth and Openid client for Microsoft Graph as part of the [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) project. This project can however be used outside [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) as an OAuth client for Microsoft Identity Platform or by using [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk). diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index 00736ff5..6e49061c 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -4,19 +4,19 @@ use std::hash::Hash; use url::{Host, Url}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum HostValidator { +pub enum HostIs { Valid, Invalid, } pub trait ValidateHosts<RHS = Self> { - fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator; + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostIs; } impl ValidateHosts for Url { - fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostIs { if valid_hosts.is_empty() { - return HostValidator::Invalid; + return HostIs::Invalid; } let size_before = valid_hosts.len(); @@ -25,37 +25,37 @@ impl ValidateHosts for Url { if let Some(host) = self.host() { if hosts.contains(&host) { - return HostValidator::Valid; + return HostIs::Valid; } } for value in valid_hosts.iter() { if !value.scheme().eq("https") { - return HostValidator::Invalid; + return HostIs::Invalid; } } - HostValidator::Invalid + HostIs::Invalid } } impl ValidateHosts for String { - fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostIs { if let Ok(url) = Url::parse(self) { return url.validate_hosts(valid_hosts); } - HostValidator::Invalid + HostIs::Invalid } } impl ValidateHosts for &str { - fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostIs { if let Ok(url) = Url::parse(self) { return url.validate_hosts(valid_hosts); } - HostValidator::Invalid + HostIs::Invalid } } @@ -75,15 +75,15 @@ impl AllowedHostValidator { AllowedHostValidator { allowed_hosts } } - pub fn validate_str(&self, url_str: &str) -> HostValidator { + pub fn validate_str(&self, url_str: &str) -> HostIs { if let Ok(url) = Url::parse(url_str) { return self.validate_hosts(&[url]); } - HostValidator::Invalid + HostIs::Invalid } - pub fn validate_url(&self, url: &Url) -> HostValidator { + pub fn validate_url(&self, url: &Url) -> HostIs { self.validate_hosts(&[url.clone()]) } } @@ -96,22 +96,19 @@ impl From<&[Url]> for AllowedHostValidator { } impl ValidateHosts for AllowedHostValidator { - fn validate_hosts(&self, valid_hosts: &[Url]) -> HostValidator { + fn validate_hosts(&self, valid_hosts: &[Url]) -> HostIs { if valid_hosts.is_empty() { - return HostValidator::Invalid; + return HostIs::Invalid; } let urls: Vec<Url> = self.allowed_hosts.iter().cloned().collect(); for url in valid_hosts.iter() { - if url - .validate_hosts(urls.as_slice()) - .eq(&HostValidator::Invalid) - { - return HostValidator::Invalid; + if url.validate_hosts(urls.as_slice()).eq(&HostIs::Invalid) { + return HostIs::Invalid; } } - HostValidator::Valid + HostIs::Valid } } @@ -161,7 +158,7 @@ mod test { assert_eq!(6, host_urls.len()); for url in host_urls.iter() { - assert_eq!(HostValidator::Valid, url.validate_hosts(&host_urls)); + assert_eq!(HostIs::Valid, url.validate_hosts(&host_urls)); } } @@ -196,10 +193,7 @@ mod test { assert_eq!(4, host_urls.len()); for url in host_urls.iter() { - assert_eq!( - HostValidator::Invalid, - url.validate_hosts(valid_hosts.as_slice()) - ); + assert_eq!(HostIs::Invalid, url.validate_hosts(valid_hosts.as_slice())); } } @@ -228,10 +222,7 @@ mod test { let allowed_host_validator = AllowedHostValidator::from(host_urls.as_slice()); for url in host_urls.iter() { - assert_eq!( - HostValidator::Valid, - allowed_host_validator.validate_url(url) - ); + assert_eq!(HostIs::Valid, allowed_host_validator.validate_url(url)); } } } diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index 5529a7cb..d8f1c27a 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -103,6 +103,7 @@ where pub struct AuthorizationResponse { pub code: Option<String>, pub id_token: Option<String>, + #[serde(default)] #[serde(deserialize_with = "deserialize_expires_in")] pub expires_in: Option<i64>, pub access_token: Option<String>, @@ -172,6 +173,10 @@ mod test { "expires_in": "3600" }"#; + pub const AUTHORIZATION_RESPONSE2: &str = r#"{ + "access_token": "token" + }"#; + #[test] pub fn deserialize_authorization_response_from_json() { let response: AuthorizationResponse = serde_json::from_str(AUTHORIZATION_RESPONSE).unwrap(); @@ -179,6 +184,13 @@ mod test { assert_eq!(Some(3600), response.expires_in); } + #[test] + pub fn deserialize_authorization_response_from_json2() { + let response: AuthorizationResponse = + serde_json::from_str(AUTHORIZATION_RESPONSE2).unwrap(); + assert_eq!(Some(String::from("token")), response.access_token); + } + #[test] pub fn deserialize_authorization_response_from_query() { let query = "access_token=token&expires_in=3600"; @@ -186,4 +198,11 @@ mod test { assert_eq!(Some(String::from("token")), response.access_token); assert_eq!(Some(3600), response.expires_in); } + + #[test] + pub fn deserialize_authorization_response_from_query_without_expires_in() { + let query = "access_token=token"; + let response: AuthorizationResponse = serde_urlencoded::from_str(query).unwrap(); + assert_eq!(Some(String::from("token")), response.access_token); + } } diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index dccc4e24..08535559 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -111,11 +111,21 @@ impl Debug for AppConfig { .finish() } else { f.debug_struct("AppConfig") - .field("tenant_id", &self.tenant_id) - .field("client_id", &self.client_id) + .field( + "tenant_id", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) + .field( + "client_id", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) .field("authority", &self.authority) .field("azure_cloud_instance", &self.azure_cloud_instance) .field("extra_query_parameters", &self.extra_query_parameters) + .field( + "extra_header_parameters", + &"[REDACTED] - call enable_pii_logging(true) to log value", + ) .field("scope", &self.scope) .field("force_token_refresh", &self.force_token_refresh) .finish() @@ -163,7 +173,7 @@ impl AppConfig { } } - pub fn log_pii(&mut self, log_pii: bool) { + pub fn enable_pii_logging(&mut self, log_pii: bool) { self.log_pii = log_pii; } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 0e71a7a9..96b83606 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -28,10 +28,11 @@ use crate::identity::{AuthorizationResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, - WebViewOptions, + HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, }; +#[cfg(feature = "interactive-auth")] +use crate::web::UserEvents; #[cfg(feature = "interactive-auth")] use wry::{ application::{event_loop::EventLoopProxy, window::Window}, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 0c7cb469..23e2147d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -31,7 +31,53 @@ credential_builder!( /// a user-agent that supports redirection from the authorization server (the Microsoft /// identity platform) back to your application. For example, a web browser, desktop, or mobile /// application operated by a user to sign in to your app and access their data. -/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow +/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow' +/// +/// [X509Certificate] requires features = \["openssl"\] +/// ```rust,ignore +/// use graph_rs_sdk::oauth::{ +/// ClientCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, X509, +/// }; +/// use std::fs::File; +/// use std::io::Read; +/// use std::path::Path; +/// +/// pub fn x509_certificate( +/// client_id: &str, +/// tenant: &str, +/// public_key_path: impl AsRef<Path>, +/// private_key_path: impl AsRef<Path>, +/// ) -> anyhow::Result<X509Certificate> { +/// // Use include_bytes!(file_path) if the files are local +/// let mut cert_file = File::open(public_key_path)?; +/// let mut certificate: Vec<u8> = Vec::new(); +/// cert_file.read_to_end(&mut certificate)?; +/// +/// let mut private_key_file = File::open(private_key_path)?; +/// let mut private_key: Vec<u8> = Vec::new(); +/// private_key_file.read_to_end(&mut private_key)?; +/// +/// let cert = X509::from_pem(certificate.as_slice())?; +/// let pkey = PKey::private_key_from_pem(private_key.as_slice())?; +/// Ok(X509Certificate::new_with_tenant( +/// client_id, tenant, cert, pkey, +/// )) +/// } +/// +/// fn build_confidential_client( +/// client_id: &str, +/// tenant: &str, +/// scope: Vec<&str>, +/// x509certificate: X509Certificate, +/// ) -> anyhow::Result<ConfidentialClientApplication<ClientCertificateCredential>> { +/// Ok(ConfidentialClientApplication::builder(client_id) +/// .with_client_x509_certificate(&x509certificate)? +/// .with_tenant(tenant) +/// .with_scope(scope) +/// .build()) +/// } +/// +/// ``` #[derive(Clone)] pub struct AuthorizationCodeCertificateCredential { pub(crate) app_config: AppConfig, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 2d2a231e..4c0e1b31 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -33,8 +33,10 @@ use graph_error::WebViewDeviceCodeError; use crate::web::WebViewOptions; #[cfg(feature = "interactive-auth")] -use crate::web::{HostOptions, InteractiveAuth, UserEvents}; +use crate::web::{HostOptions, InteractiveAuth}; +#[cfg(feature = "interactive-auth")] +use crate::web::UserEvents; use graph_core::identity::ForceTokenRefresh; #[cfg(feature = "interactive-auth")] use wry::{ diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 2d14c67f..7b968b15 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -24,10 +24,11 @@ use crate::identity::{AuthorizationResponse, Token}; #[cfg(feature = "interactive-auth")] use crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, - WebViewOptions, + HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, }; +#[cfg(feature = "interactive-auth")] +use crate::web::UserEvents; #[cfg(feature = "interactive-auth")] use wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -199,6 +200,7 @@ impl OpenIdAuthorizationUrlParameters { &self.nonce } + #[tracing::instrument] #[cfg(feature = "interactive-auth")] pub fn interactive_webview_authentication( &self, diff --git a/graph-oauth/src/identity/device_authorization_response.rs b/graph-oauth/src/identity/device_authorization_response.rs index 95c8251b..4f618734 100644 --- a/graph-oauth/src/identity/device_authorization_response.rs +++ b/graph-oauth/src/identity/device_authorization_response.rs @@ -20,7 +20,7 @@ use crate::identity::{DeviceCodeCredential, PublicClientApplication}; /// /// The actual [device code response](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code#device-authorization-response) /// that is received from Microsoft Graph does not include the verification_uri_complete field -/// that the [specification](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2) +/// even though it's in the [specification](https://datatracker.ietf.org/doc/html/rfc8628#section-3.2). /// The device code response from Microsoft Graph looks like similar to the following: /// /// ```json diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_authenticator.rs index b522db84..178aad7d 100644 --- a/graph-oauth/src/web/interactive_authenticator.rs +++ b/graph-oauth/src/web/interactive_authenticator.rs @@ -1,4 +1,4 @@ -use crate::web::{HostOptions, UserEvents, WebViewOptions}; +use crate::web::{HostOptions, WebViewOptions}; use graph_error::WebViewResult; use std::fmt::{Debug, Display, Formatter}; use std::sync::mpsc::Sender; @@ -9,9 +9,44 @@ use wry::application::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, Eve use wry::application::window::{Window, WindowBuilder}; use wry::webview::WebView; +#[cfg(target_family = "unix")] +use wry::application::platform::unix::EventLoopBuilderExtUnix; + #[cfg(target_family = "windows")] use wry::application::platform::windows::EventLoopBuilderExtWindows; +#[derive(Clone, Debug)] +pub enum WindowCloseReason { + CloseRequested, + TimedOut { + start: Instant, + requested_resume: Instant, + }, +} + +impl Display for WindowCloseReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + WindowCloseReason::CloseRequested => write!(f, "CloseRequested"), + WindowCloseReason::TimedOut { .. } => write!(f, "TimedOut"), + } + } +} + +#[derive(Clone, Debug)] +pub enum InteractiveAuthEvent { + InvalidRedirectUri(String), + ReachedRedirectUri(Url), + WindowClosed(WindowCloseReason), +} + +#[derive(Debug, Clone)] +pub enum UserEvents { + CloseWindow, + InternalCloseWindow, + ReachedRedirectUri(Url), +} + pub trait InteractiveAuthenticator { fn interactive_authentication( &self, @@ -125,40 +160,9 @@ where .with_resizable(true) } - #[cfg(target_family = "windows")] fn event_loop() -> EventLoop<UserEvents> { EventLoopBuilder::with_user_event() .with_any_thread(true) .build() } - - #[cfg(target_family = "unix")] - fn event_loop() -> EventLoop<UserEvents> { - EventLoopBuilder::with_user_event().build() - } -} - -#[derive(Clone, Debug)] -pub enum WindowCloseReason { - CloseRequested, - TimedOut { - start: Instant, - requested_resume: Instant, - }, -} - -impl Display for WindowCloseReason { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - WindowCloseReason::CloseRequested => write!(f, "CloseRequested"), - WindowCloseReason::TimedOut { .. } => write!(f, "TimedOut"), - } - } -} - -#[derive(Clone, Debug)] -pub enum InteractiveAuthEvent { - InvalidRedirectUri(String), - ReachedRedirectUri(Url), - WindowClosed(WindowCloseReason), } diff --git a/graph-oauth/src/web/interactive_web_view.rs b/graph-oauth/src/web/interactive_web_view.rs deleted file mode 100644 index d9c604b7..00000000 --- a/graph-oauth/src/web/interactive_web_view.rs +++ /dev/null @@ -1,312 +0,0 @@ -use std::collections::HashSet; -use std::time::Duration; -use url::Url; - -use crate::oauth::InteractiveDeviceCodeEvent; -use crate::web::{HostOptions, InteractiveAuthEvent, WebViewOptions, WindowCloseReason}; -use graph_error::{WebViewError, WebViewResult}; -use wry::application::event_loop::EventLoopBuilder; -use wry::application::platform::windows::EventLoopBuilderExtWindows; -use wry::{ - application::{ - event::{Event, StartCause, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, - }, - webview::WebViewBuilder, -}; - -#[derive(Debug, Clone)] -pub enum UserEvents { - CloseWindow, - InternalCloseWindow, - ReachedRedirectUri(Url), -} - -pub(crate) struct WebViewHostValidator { - start_uri: Url, - redirect_uris: Vec<Url>, - ports: HashSet<usize>, - is_local_host: bool, -} - -impl WebViewHostValidator { - pub fn new( - start_uri: Url, - redirect_uris: Vec<Url>, - ports: HashSet<usize>, - ) -> WebViewResult<WebViewHostValidator> { - if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { - return Err(WebViewError::InvalidUri( - "Authorization url and redirect uri must have valid uri hosts".into(), - )); - } - - let is_local_host = redirect_uris - .iter() - .any(|uri| uri.as_str().eq("http://localhost")); - - if is_local_host && ports.is_empty() { - return Err(WebViewError::InvalidUri( - "Redirect uri is http://localhost but not ports were specified".into(), - )); - } - - Ok(WebViewHostValidator { - start_uri, - redirect_uris, - ports, - is_local_host, - }) - } - - pub fn is_valid_uri(&self, url: &Url) -> bool { - if let Some(host) = url.host() { - if self.is_local_host && !self.ports.is_empty() { - let hosts: Vec<url::Host> = self - .redirect_uris - .iter() - .map(|port| url::Host::parse(&format!("http://localhost:{}", port)).unwrap()) - .collect(); - - for redirect_uri in self.redirect_uris.iter() { - if let Some(redirect_uri_host) = redirect_uri.host() { - if hosts.iter().any(|host| host.eq(&redirect_uri_host)) { - return true; - } - } - } - } - - self.start_uri.host().eq(&Some(host.clone())) - || self - .redirect_uris - .iter() - .any(|uri| uri.host().eq(&Some(host.clone()))) - } else { - false - } - } - - pub fn is_redirect_host(&self, url: &Url) -> bool { - if let Some(host) = url.host() { - self.redirect_uris - .iter() - .any(|uri| uri.host().eq(&Some(host.clone()))) - } else { - false - } - } -} - -impl TryFrom<HostOptions> for WebViewHostValidator { - type Error = WebViewError; - - fn try_from(value: HostOptions) -> Result<Self, Self::Error> { - WebViewHostValidator::new(value.start_uri, value.redirect_uris, value.ports) - } -} - -pub struct InteractiveWebView; - -impl InteractiveWebView { - #[tracing::instrument] - pub fn interactive_authentication( - uri: Url, - redirect_uris: Vec<Url>, - options: WebViewOptions, - sender: std::sync::mpsc::Sender<InteractiveAuthEvent>, - ) -> anyhow::Result<()> { - tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); - let validator = WebViewHostValidator::new(uri.clone(), redirect_uris, options.ports)?; - let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() - .with_any_thread(true) - .build(); - let proxy = event_loop.create_proxy(); - let sender2 = sender.clone(); - - let window = WindowBuilder::new() - .with_title(options.window_title) - .with_closable(true) - .with_content_protection(true) - .with_minimizable(true) - .with_maximizable(true) - .with_focused(true) - .with_resizable(true) - .with_theme(options.theme) - .build(&event_loop)?; - - let webview = WebViewBuilder::new(window)? - .with_url(uri.as_ref())? - // Disables file drop - .with_file_drop_handler(|_, _| true) - .with_navigation_handler(move |uri| { - if let Ok(url) = Url::parse(uri.as_str()) { - let is_valid_host = validator.is_valid_uri(&url); - let is_redirect = validator.is_redirect_host(&url); - - if is_redirect { - sender2 - .send(InteractiveAuthEvent::ReachedRedirectUri(url.clone())) - .unwrap_or_default(); - // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. - - let _ = proxy.send_event(UserEvents::ReachedRedirectUri(url)); - return true; - } - - is_valid_host - } else { - tracing::debug!(target: "interactive_webview", "Unable to navigate WebView - Option<Url> was None"); - let _ = proxy.send_event(UserEvents::CloseWindow); - false - } - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - if let Some(timeout) = options.timeout.as_ref() { - *control_flow = ControlFlow::WaitUntil(*timeout); - } else { - *control_flow = ControlFlow::Wait; - } - - match event { - Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), - Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { - sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { - start, requested_resume - })).unwrap_or_default(); - tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); - tracing::trace!(target: "interactive_webview", "Window closing before reaching redirect uri"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::InternalCloseWindow) => { - tracing::trace!(target: "interactive_webview", "Matched on redirect uri: {uri:#?} - Closing window"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - _ => (), - } - }); - } - - #[tracing::instrument] - pub fn device_code_interactive_authentication( - uri: Url, - options: WebViewOptions, - sender: std::sync::mpsc::Sender<InteractiveDeviceCodeEvent>, - ) -> anyhow::Result<()> { - tracing::trace!(target: "interactive_webview", "Constructing WebView Window and EventLoop"); - let event_loop: EventLoop<UserEvents> = EventLoopBuilder::with_user_event() - .with_any_thread(true) - .build(); - - let window = WindowBuilder::new() - .with_title(options.window_title) - .with_closable(true) - .with_content_protection(true) - .with_minimizable(true) - .with_maximizable(true) - .with_focused(true) - .with_resizable(true) - .with_theme(options.theme) - .build(&event_loop)?; - - let webview = WebViewBuilder::new(window)? - .with_url(uri.as_ref())? - // Disables file drop - .with_file_drop_handler(|_, _| true) - .with_navigation_handler(move |uri| { - if let Ok(url) = Url::parse(uri.as_str()) { - tracing::event!(tracing::Level::INFO, url = url.as_str()); - } - true - }) - .build()?; - - event_loop.run(move |event, _, control_flow| { - if let Some(timeout) = options.timeout.as_ref() { - *control_flow = ControlFlow::WaitUntil(*timeout); - } else { - *control_flow = ControlFlow::Wait; - } - - match event { - Event::NewEvents(StartCause::Init) => tracing::debug!(target: "interactive_webview", "Webview runtime started"), - Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { - sender.send(InteractiveDeviceCodeEvent::WindowClosed( - WindowCloseReason::TimedOut { - start, requested_resume - } - )).unwrap_or_default(); - - tracing::debug!(target: "interactive_webview", "Timeout reached - closing window"); - - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => { - sender.send(InteractiveDeviceCodeEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); - if options.clear_browsing_data { - let _ = webview.clear_all_browsing_data(); - } - - // Wait time to avoid deadlock where window closes before receiver gets the event - std::thread::sleep(Duration::from_millis(500)); - *control_flow = ControlFlow::Exit - } - _ => (), - } - }); - } -} diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs index 904c2db8..c1eb141e 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/web/mod.rs @@ -1,7 +1,6 @@ mod interactive_authenticator; -mod interactive_web_view; -mod web_view_options; +mod webview_host_validator; +mod webview_options; pub use interactive_authenticator::*; -pub use interactive_web_view::*; -pub use web_view_options::*; +pub use webview_options::*; diff --git a/graph-oauth/src/web/webview_host_validator.rs b/graph-oauth/src/web/webview_host_validator.rs new file mode 100644 index 00000000..9f50aeee --- /dev/null +++ b/graph-oauth/src/web/webview_host_validator.rs @@ -0,0 +1,89 @@ +use std::collections::HashSet; +use url::Url; + +use crate::web::HostOptions; +use graph_error::{WebViewError, WebViewResult}; + +pub(crate) struct WebViewHostValidator { + start_uri: Url, + redirect_uris: Vec<Url>, + ports: HashSet<usize>, + is_local_host: bool, +} + +impl WebViewHostValidator { + pub fn new( + start_uri: Url, + redirect_uris: Vec<Url>, + ports: HashSet<usize>, + ) -> WebViewResult<WebViewHostValidator> { + if start_uri.host().is_none() || redirect_uris.iter().any(|uri| uri.host().is_none()) { + return Err(WebViewError::InvalidUri( + "Authorization url and redirect uri must have valid uri hosts".into(), + )); + } + + let is_local_host = redirect_uris + .iter() + .any(|uri| uri.as_str().eq("http://localhost")); + + if is_local_host && ports.is_empty() { + return Err(WebViewError::InvalidUri( + "Redirect uri is http://localhost but not ports were specified".into(), + )); + } + + Ok(WebViewHostValidator { + start_uri, + redirect_uris, + ports, + is_local_host, + }) + } + + pub fn is_valid_uri(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + if self.is_local_host && !self.ports.is_empty() { + let hosts: Vec<url::Host> = self + .redirect_uris + .iter() + .map(|port| url::Host::parse(&format!("http://localhost:{}", port)).unwrap()) + .collect(); + + for redirect_uri in self.redirect_uris.iter() { + if let Some(redirect_uri_host) = redirect_uri.host() { + if hosts.iter().any(|host| host.eq(&redirect_uri_host)) { + return true; + } + } + } + } + + self.start_uri.host().eq(&Some(host.clone())) + || self + .redirect_uris + .iter() + .any(|uri| uri.host().eq(&Some(host.clone()))) + } else { + false + } + } + + pub fn is_redirect_host(&self, url: &Url) -> bool { + if let Some(host) = url.host() { + self.redirect_uris + .iter() + .any(|uri| uri.host().eq(&Some(host.clone()))) + } else { + false + } + } +} + +impl TryFrom<HostOptions> for WebViewHostValidator { + type Error = WebViewError; + + fn try_from(value: HostOptions) -> Result<Self, Self::Error> { + WebViewHostValidator::new(value.start_uri, value.redirect_uris, value.ports) + } +} diff --git a/graph-oauth/src/web/web_view_options.rs b/graph-oauth/src/web/webview_options.rs similarity index 88% rename from graph-oauth/src/web/web_view_options.rs rename to graph-oauth/src/web/webview_options.rs index 441d0145..165520fa 100644 --- a/graph-oauth/src/web/web_view_options.rs +++ b/graph-oauth/src/web/webview_options.rs @@ -4,7 +4,7 @@ use url::Url; pub use wry::application::window::Theme; -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct HostOptions { pub(crate) start_uri: Url, pub(crate) redirect_uris: Vec<Url>, @@ -37,6 +37,8 @@ pub struct WebViewOptions { pub window_title: String, /// OS specific theme. Only available on Windows. /// See wry crate for more info. + /// + /// Theme is not set by default. #[cfg(windows)] pub theme: Option<Theme>, /// Provide a list of ports to use for interactive authentication. @@ -47,10 +49,14 @@ pub struct WebViewOptions { /// when that timeout is reached. For instance, if your app is waiting on the /// user to log in and the user has not logged in after 20 minutes you may /// want to assume the user is idle in some way and close out of the webview window. + /// + /// Default is no timeout. pub timeout: Option<Instant>, /// The webview can store the cookies that were set after sign in so that on the next /// sign in the user is automatically logged in through SSO. Or you can clear the browsing /// data, cookies in this case, after sign in when the webview window closes. + /// + /// Default is true pub clear_browsing_data: bool, } @@ -96,15 +102,27 @@ impl WebViewOptions { } } +#[cfg(windows)] impl Default for WebViewOptions { fn default() -> Self { WebViewOptions { window_title: "Sign In".to_string(), theme: None, ports: Default::default(), - // 10 Minutes default timeout timeout: None, - clear_browsing_data: false, + clear_browsing_data: true, + } + } +} + +#[cfg(unix)] +impl Default for WebViewOptions { + fn default() -> Self { + WebViewOptions { + window_title: "Sign In".to_string(), + ports: Default::default(), + timeout: None, + clear_browsing_data: true, } } } diff --git a/src/client/graph.rs b/src/client/graph.rs index 2288c2cb..8a4a9143 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -45,11 +45,12 @@ use crate::identity_governance::IdentityGovernanceApiClient; use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; use crate::invitations::InvitationsApiClient; use crate::me::MeApiClient; -use crate::oauth::{AllowedHostValidator, HostValidator, Token}; use crate::oauth::{ - AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, - AuthorizationCodeCredential, BearerTokenCredential, ClientAssertionCredential, - ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, + AllowedHostValidator, AuthorizationCodeAssertionCredential, + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, BearerTokenCredential, + ClientAssertionCredential, ClientCertificateCredential, ClientSecretCredential, + ConfidentialClientApplication, DeviceCodeCredential, HostIs, OpenIdCredential, + PublicClientApplication, ResourceOwnerPasswordCredential, Token, }; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, @@ -71,10 +72,6 @@ use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; use graph_core::identity::ForceTokenRefresh; -use graph_oauth::oauth::{ - DeviceCodeCredential, OpenIdCredential, PublicClientApplication, - ResourceOwnerPasswordCredential, -}; use lazy_static::lazy_static; lazy_static! { @@ -126,7 +123,7 @@ impl GraphClient { /// .send() /// .await?; /// ``` - pub fn v1(&mut self) -> &mut Graph { + pub fn v1(&mut self) -> &mut GraphClient { self.endpoint = PARSED_GRAPH_URL.clone(); self } @@ -164,7 +161,7 @@ impl GraphClient { /// .send() /// .await?; /// ``` - pub fn beta(&mut self) -> &mut Graph { + pub fn beta(&mut self) -> &mut GraphClient { self.endpoint = PARSED_GRAPH_URL_BETA.clone(); self } @@ -199,7 +196,7 @@ impl GraphClient { self } - pub fn set_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { + pub fn use_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { self.client.with_force_token_refresh(force_token_refresh); } @@ -241,7 +238,7 @@ impl GraphClient { /// .send() /// .await?; /// ``` - pub fn custom_endpoint(&mut self, custom_endpoint: &str) -> &mut Graph { + pub fn custom_endpoint(&mut self, custom_endpoint: &str) -> &mut GraphClient { self.use_endpoint(custom_endpoint); self } @@ -283,7 +280,7 @@ impl GraphClient { /// ``` pub fn use_endpoint(&mut self, custom_endpoint: &str) { match self.allowed_host_validator.validate_str(custom_endpoint) { - HostValidator::Valid => { + HostIs::Valid => { let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); if url.query().is_some() { @@ -295,7 +292,7 @@ impl GraphClient { self.endpoint.set_host(url.host_str()).unwrap(); self.endpoint.set_path(url.path()); } - HostValidator::Invalid => panic!("Invalid host"), + HostIs::Invalid => panic!("Invalid host"), } } @@ -560,7 +557,7 @@ impl From<&Token> for GraphClient { impl From<GraphClientConfiguration> for GraphClient { fn from(graph_client_builder: GraphClientConfiguration) -> Self { GraphClient { - client: graph_client_builder.build(), + client: Client::from(graph_client_builder), endpoint: PARSED_GRAPH_URL.clone(), allowed_host_validator: AllowedHostValidator::default(), } diff --git a/tests/mail_folder_request.rs b/tests/mail_folder_request.rs index b954f794..fb72cebc 100644 --- a/tests/mail_folder_request.rs +++ b/tests/mail_folder_request.rs @@ -1,5 +1,5 @@ use graph_http::api_impl::ODataQuery; -use test_tools::oauth_request::{DEFAULT_ONENOTE_CREDENTIALS_MUTEX}; +use test_tools::oauth_request::DEFAULT_ONENOTE_CREDENTIALS_MUTEX; #[ignore] #[tokio::test] From a4a29f01b4fe3508d9af8292989082a8bcb1bb6b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:39:51 -0500 Subject: [PATCH 071/118] Use JWKS verification for openid --- .../{README.md => INTERACTIVE_AUTH.md} | 0 examples/interactive_auth/webview_errors.rs | 49 ------------------- graph-core/src/crypto/jwk.rs | 0 ...e_authenticator.rs => interactive_auth.rs} | 0 4 files changed, 49 deletions(-) rename examples/interactive_auth/{README.md => INTERACTIVE_AUTH.md} (100%) delete mode 100644 examples/interactive_auth/webview_errors.rs create mode 100644 graph-core/src/crypto/jwk.rs rename graph-oauth/src/web/{interactive_authenticator.rs => interactive_auth.rs} (100%) diff --git a/examples/interactive_auth/README.md b/examples/interactive_auth/INTERACTIVE_AUTH.md similarity index 100% rename from examples/interactive_auth/README.md rename to examples/interactive_auth/INTERACTIVE_AUTH.md diff --git a/examples/interactive_auth/webview_errors.rs b/examples/interactive_auth/webview_errors.rs deleted file mode 100644 index 52e52b43..00000000 --- a/examples/interactive_auth/webview_errors.rs +++ /dev/null @@ -1,49 +0,0 @@ -use graph_rs_sdk::{error::WebViewError, oauth::AuthorizationCodeCredential}; - -async fn interactive_auth(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { - let mut credential_builder_result = - AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(scope) - .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(Default::default()); - - if let Ok((authorization_query_response, credential_builder)) = credential_builder_result { - // ... - } else if let Err(err) = credential_builder_result { - match err { - // Webview Window closed for one of the following reasons: - // 1. The user closed the webview window without logging in. - // 2. The webview exited because of a timeout defined in the WebViewOptions. - // - // Values will be one of: - // 1. CloseRequested: User closed the window before completing sign in and redirect. - // 2. TimedOut: The timeout specified in WebViewOptions was reached. By default there - // is no timeout. - WebViewError::WindowClosed(reason) => {} - - // One of the following errors has occurred: - // - // 1. Issues with the redirect uri such as specifying localhost - // but not providing a port in the WebViewOptions. - // - // 2. The webview was successfully redirected but the url did not - // contain a query or fragment. The query or fragment of the url - // is where the auth code would be returned to the app. - // - // 3. The host or domain provided or set for login is invalid. - // This could be an internal error and most likely will never happen. - WebViewError::InvalidUri(reason) => {} - - // The query or fragment of the redirect uri is an error returned - // from Microsoft. - WebViewError::AuthorizationQuery { - error, - error_description, - error_uri, - } => {} - - WebViewError::AuthExecutionError(_) => {} - } - } -} diff --git a/graph-core/src/crypto/jwk.rs b/graph-core/src/crypto/jwk.rs new file mode 100644 index 00000000..e69de29b diff --git a/graph-oauth/src/web/interactive_authenticator.rs b/graph-oauth/src/web/interactive_auth.rs similarity index 100% rename from graph-oauth/src/web/interactive_authenticator.rs rename to graph-oauth/src/web/interactive_auth.rs From e9d89e7693b584b690980b7c8ec65cb7e83d0de1 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Wed, 6 Dec 2023 00:40:07 -0500 Subject: [PATCH 072/118] Use JWKS verification for openid --- Cargo.toml | 1 + examples/interactive_auth/INTERACTIVE_AUTH.md | 230 ++++++++++++-- examples/interactive_auth/auth_code.rs | 11 +- examples/interactive_auth/main.rs | 1 - examples/interactive_auth/openid.rs | 8 +- examples/interactive_auth/webview_options.rs | 8 +- .../auth_code_grant/auth_code_certificate.rs | 2 +- .../auth_code_grant/server_example/mod.rs | 2 +- graph-core/src/crypto/jwk.rs | 119 ++++++++ graph-core/src/crypto/mod.rs | 2 + graph-error/src/webview_error.rs | 17 +- graph-oauth/Cargo.toml | 1 + graph-oauth/src/identity/authority.rs | 12 + .../identity/authorization_query_response.rs | 100 +++++++ .../credentials/application_builder.rs | 2 +- .../auth_code_authorization_url.rs | 283 +++++++++++------- ...authorization_code_assertion_credential.rs | 13 +- ...thorization_code_certificate_credential.rs | 41 ++- .../authorization_code_credential.rs | 169 ++++++----- .../client_assertion_credential.rs | 108 +++---- .../client_certificate_credential.rs | 35 +-- .../client_credentials_authorization_url.rs | 18 +- .../credentials/client_secret_credential.rs | 18 +- .../credentials/device_code_credential.rs | 107 +++---- graph-oauth/src/identity/credentials/mod.rs | 6 +- .../credentials/open_id_authorization_url.rs | 93 +++--- .../credentials/open_id_credential.rs | 144 ++++++++- .../resource_owner_password_credential.rs | 15 +- .../src/identity/credentials/response_type.rs | 14 +- .../credentials/token_credential_executor.rs | 14 +- .../identity/credentials/x509_certificate.rs | 22 +- graph-oauth/src/identity/id_token.rs | 132 +++++++- graph-oauth/src/identity/token.rs | 62 +++- graph-oauth/src/web/interactive_auth.rs | 72 +++-- graph-oauth/src/web/mod.rs | 5 +- 35 files changed, 1383 insertions(+), 504 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a310d114..882431d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,7 @@ webbrowser = "0.8.7" anyhow = "1.0.69" log = "0.4" pretty_env_logger = "0.5.0" +base64 = "0.21.0" graph-codegen = { path = "./graph-codegen", version = "0.0.1" } test-tools = { path = "./test-tools", version = "0.0.1" } diff --git a/examples/interactive_auth/INTERACTIVE_AUTH.md b/examples/interactive_auth/INTERACTIVE_AUTH.md index e9485616..e8b2a0cf 100644 --- a/examples/interactive_auth/INTERACTIVE_AUTH.md +++ b/examples/interactive_auth/INTERACTIVE_AUTH.md @@ -7,13 +7,15 @@ Interactive Authentication uses a webview provided by the Wry crate https://gith See the wry documentation for platform specific installation. Linux and macOS require installation of platform specific dependencies. These are not included by default. -This example executes the Authorization Code OAuth flow and handles -sign in/redirect using WebView as well as authorization and token retrieval. +The examples below executes the Authorization Code OAuth flow and handles +sign in/redirect using WebView as well as execution of the token requests. -The WebView window will load on the sign in page for Microsoft Graph -Log in with a user and upon redirect the will close the window automatically. -The credential_builder will store the authorization code returned on the -redirect url after logging in and then build a `ConfidentialClient<AuthorizationCodeCredential>` +The WebView window will load on the sign in page for Microsoft Graph. +You can Log in with a user and upon redirect the WebView Window will close automatically. + +The `CredentialBuilder` that is returned stores the authorization code returned on the +redirect url after logging in. You can use the `CredentialBuilder` to build a +`ConfidentialClient<AuthorizationCodeCredential>` which can be passed to the `GraphClient` The `ConfidentialClient<AuthorizationCodeCredential>` handles authorization to get an access token on the first request made using the Graph client. The token is stored in an in memory cache @@ -21,36 +23,99 @@ and subsequent calls will use this token. If a refresh token is included, which by requesting the offline_access scope, then the confidential client will take care of refreshing the token. -### Example +The Auth Code Grant can be performed using a client secret, a certificate, or an assertion. + +- Client Secret: + +Requires `features = ["interactive-auth"]` + +`CredentialBuilder` returned is `AuthorizationCodeCredentialBuilder` + +```rust +async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri)? + .with_interactive_auth(Default::default())? + .into_result()?; + + println!("{authorization_response:#?}"); + + Ok(credential_builder) +} +``` + +- Certificate + +Requires `features = ["interactive-auth", "openssl"]` + +`CredentialBuilder` returned is `AuthorizationCodeCertificateCredentialBuilder` ```rust -static CLIENT_ID: &str = "CLIENT_ID"; -static CLIENT_SECRET: &str = "CLIENT_SECRET"; -static TENANT_ID: &str = "TENANT_ID"; +async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri)? + .with_certificate_interactive_auth(Default::default(), x509)? + .into_result()?; + + println!("{authorization_response:#?}"); -// This should be the user id for the user you are logging in as. -static USER_ID: &str = "USER_ID"; + Ok(credential_builder) +} +``` -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +- Assertion -async fn authenticate() { - std::env::set_var("RUST_LOG", "debug"); - pretty_env_logger::init(); +Requires `features = ["interactive-auth"]` - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_tenant(TENANT_ID) +`CredentialBuilder` returned is `AuthorizationCodeAssertionCredentialBuilder` + +```rust +async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeAssertionCredentialBuilder> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(REDIRECT_URI) - .with_interactive_authentication(Default::default()) - .unwrap(); + .with_redirect_uri(redirect_uri)? + .with_assertion_interactive_auth(Default::default())? + .into_result()?; - let mut confidential_client = credential_builder.with_client_secret(CLIENT_SECRET).build(); + println!("{authorization_response:#?}"); - let client = GraphClient::from(&confidential_client); + Ok(credential_builder) } ``` +### Convenience Methods + +The `into_result` method transforms the `AuthorizationEvent` that is normally returned from +`with_interactive_authentication` into `(AuthorizationResponse, CredentialBuilder)`. + +By default `with_interactive_authentication` returns `AuthorizationEvent<CredentialBuilder>` which can provide +the caller with useful information about the events happening with the webview such as if the user closed the window. + +For those that don't necessarily care about those events use `into_result` to transform the `AuthorizationEvent` +into the credential builder that can be built and passed to the `GraphClient`. + +See [Reacting To Events](#reacting-to-events) to learn more. + +```rust +async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<()> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read"]) + .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. + .with_redirect_uri(redirect_uri)? + .with_interactive_authentication(Default::default())? + .into_result()?; + Ok(()) +} +``` ### WebView Options @@ -66,7 +131,7 @@ fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" .with_window_title("Sign In") - // OS specific theme. Does not work on all operating systems. + // OS specific theme. Windows Only. // See wry crate for more info. .with_theme(Theme::Dark) // Add a timeout that will close the window and return an error @@ -84,12 +149,119 @@ fn get_webview_options() -> WebViewOptions { .with_ports(&[8000]) } -async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) { - let mut credential_builder = AuthorizationCodeCredential::authorization_url_builder(client_id) +async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<()> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(scope) + .with_redirect_uri(redirect_uri)? + .with_interactive_authentication(get_webview_options())? + .into_result()?; + + Ok(()) +} +``` + +### Reacting To Events + +By default `with_interactive_authentication` returns `AuthorizationEvent<CredentialBuilder>` which can provide +the caller with useful information about the events happening with the webview such as if the user closed the window. + +For those that don't necessarily care about those events use `into_result` to transform the `AuthorizationEvent` +into the credential builder that can be built and passed to the `GraphClient`. + +```rust +fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<GraphClient> { + let authorization_event = + OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read", "offline_access", "email", "profile"]) + .with_response_mode(ResponseMode::Fragment) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_redirect_uri(redirect_uri)? + .with_interactive_auth_for_secret(Default::default())?; + + match authorization_event { + AuthorizationEvent::Authorized { authorization_response, mut credential_builder } => { + println!("{authorization_response:#?}"); + + let mut confidential_client = credential_builder + .with_client_secret(client_secret) + .build(); + + Ok(GraphClient::from(&confidential_client)) + } + AuthorizationEvent::Unauthorized(authorization_response) => { + println!("{authorization_response:#?}"); + Err(anyhow!(format!("error: {:#?}, error_description: {:#?}, error_uri: {:#?}", authorization_response.error, authorization_response.error_description, authorization_response.error_uri))) + } + AuthorizationEvent::Impeded(AuthorizationImpeded::WindowClosed(reason)) => { + println!("Authorization Impeded With Reason: {reason:#?}"); + Err(anyhow!(reason)) + } + AuthorizationEvent::Impeded(AuthorizationImpeded::InvalidUri(reason)) => { + println!("Authorization Impeded With Reason: {reason:#?}"); + Err(anyhow!(reason)) + } + } +} +``` + +The `Unauthorized` and `Impeded` variants of `AuthorizationImpeded` useful for error handling inside an application. + +1. `AuthorizationImpeded::WindowClosed(reason: String)` + Where reason is one of: + - CloseRequested: The user closed the window before finishing login. + - TimedOut: A timeout was reached that you can set in `WebViewOptions`. Suppose your waiting on the user to sign + in but the window has been idle (no user interaction) for 20 minutes. You can specify a timeout such as 20 minutes + to close the window and perform some other work. + - WindowDestroyed: Either the window was destroyed or the webview event loop was destroyed causing the window + to also be destroyed. The cause is unknown. The login did not finish. +2. `AuthorizationImpeded::InvalidUri(reason)` + - Where reason is a message for why the URI is invalid and the URI itself. + + +The third variant `Authorized` means that the query or fragment of the URL was successfully parsed +from a redirect after sign in. This variant provides the parsed `AuthorizationResponse` from the +query or fragment and the `CredentialBuilder` that you can build and pass to the `GraphClient`: + +```rust +fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<GraphClient> { + let authorization_event = + OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read", "offline_access", "email", "profile"]) + .with_response_mode(ResponseMode::Fragment) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_redirect_uri(redirect_uri)? + .with_interactive_auth_for_secret(Default::default())?; + + match authorization_event { + AuthorizationEvent::Authorized { authorization_response, mut credential_builder } => { + println!("{authorization_response:#?}"); + + let mut confidential_client = credential_builder + .with_client_secret(client_secret) + .build(); + + Ok(GraphClient::from(&confidential_client)) + } + _ => Err(anyhow!("failed")) + } +} +``` + +Using `into_result` transforms the `Unauthorized` and `Impeded` variants of `AuthorizationEvent` +into `WebViewError` which is then returned in the result `Result<(AuthorizationResponse, CredentialBuilder), WebViewError>` + +```rust +async fn authenticate(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<()> { + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .with_interactive_authentication(Some(get_webview_options())) - .unwrap(); + .with_redirect_uri(redirect_uri)? + .with_interactive_auth_for_secret(Default::default())? + .into_result()?; + + Ok(()) } ``` diff --git a/examples/interactive_auth/auth_code.rs b/examples/interactive_auth/auth_code.rs index 91f569b2..aa1dfc7f 100644 --- a/examples/interactive_auth/auth_code.rs +++ b/examples/interactive_auth/auth_code.rs @@ -1,4 +1,5 @@ use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; +use url::Url; // Requires feature=interactive_authentication @@ -29,17 +30,17 @@ async fn authenticate( std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); - let (authorization_query_response, mut credential_builder) = + let (authorization_query_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(Default::default()) - .unwrap(); + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth_for_secret(client_secret, Default::default())? + .into_result()?; debug!("{authorization_query_response:#?}"); - let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + let confidential_client = credential_builder.build(); Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/interactive_auth/main.rs b/examples/interactive_auth/main.rs index 6fff785a..081dcd5c 100644 --- a/examples/interactive_auth/main.rs +++ b/examples/interactive_auth/main.rs @@ -5,7 +5,6 @@ extern crate pretty_env_logger; extern crate log; mod auth_code; mod openid; -mod webview_errors; mod webview_options; #[tokio::main] diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index ae3dda63..e241aa05 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -2,6 +2,7 @@ use graph_rs_sdk::{ oauth::{OpenIdCredential, ResponseMode, ResponseType}, GraphClient, }; +use url::Url; async fn openid_authenticate( tenant_id: &str, @@ -18,12 +19,13 @@ async fn openid_authenticate( .with_scope(vec!["user.read", "offline_access", "profile", "email"]) // Adds offline_access as a scope which is needed to get a refresh token. .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_redirect_uri(redirect_uri)? - .with_interactive_authentication(Default::default())?; + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth(client_secret, Default::default())? + .into_result()?; debug!("{authorization_query_response:#?}"); - let confidential_client = credential_builder.with_client_secret(client_secret).build(); + let confidential_client = credential_builder.build(); Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/interactive_auth/webview_options.rs b/examples/interactive_auth/webview_options.rs index 5eeb59bc..1a7ddeee 100644 --- a/examples/interactive_auth/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -3,6 +3,7 @@ use graph_rs_sdk::GraphClient; use std::collections::HashSet; use std::ops::Add; use std::time::{Duration, Instant}; +use url::Url; #[cfg(windows)] fn get_webview_options() -> WebViewOptions { @@ -58,10 +59,11 @@ async fn customize_webview( AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(get_webview_options())?; + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth_for_secret(client_secret, get_webview_options())? + .into_result()?; - let confidential_client = credential_builder.with_client_secret(client_secret).build(); + let confidential_client = credential_builder.build(); Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs b/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs index e3f9ec55..0a730da9 100644 --- a/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs +++ b/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs @@ -38,7 +38,7 @@ fn build_confidential_client( x509certificate: X509Certificate, ) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { Ok(ConfidentialClientApplication::builder(client_id) - .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? + .with_auth_code_x509_certificate(authorization_code, &x509certificate)? .with_tenant(tenant) .with_scope(scope) .with_redirect_uri(redirect_uri)? diff --git a/examples/oauth_certificate/auth_code_grant/server_example/mod.rs b/examples/oauth_certificate/auth_code_grant/server_example/mod.rs index b097af06..11161101 100644 --- a/examples/oauth_certificate/auth_code_grant/server_example/mod.rs +++ b/examples/oauth_certificate/auth_code_grant/server_example/mod.rs @@ -85,7 +85,7 @@ fn build_confidential_client( x509certificate: X509Certificate, ) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { Ok(ConfidentialClientApplication::builder(CLIENT_ID) - .with_authorization_code_x509_certificate(authorization_code, &x509certificate)? + .with_auth_code_x509_certificate(authorization_code, &x509certificate)? .with_tenant(TENANT) .with_scope(vec![SCOPE]) .with_redirect_uri(REDIRECT_URI)? diff --git a/graph-core/src/crypto/jwk.rs b/graph-core/src/crypto/jwk.rs index e69de29b..0a4c4576 100644 --- a/graph-core/src/crypto/jwk.rs +++ b/graph-core/src/crypto/jwk.rs @@ -0,0 +1,119 @@ +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; +use url::Url; + +/// JSON Web Key (JWK) is a JSON object that represents a cryptographic key. +/// The members of the object represent properties of the key, including its value. +/// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4) +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct JsonWebKey { + /// The "kty" (key type) parameter identifies the cryptographic algorithm family used with + /// the key, such as "RSA" or "EC". "kty" values should either be registered in the + /// IANA "JSON Web Key Types" registry established by [JWA] or be a value that contains + /// a Collision-Resistant Name. The "kty" value is a case-sensitive string. + /// This member MUST be present in a JWK. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.1) + pub kty: String, + + /// The "use" (public key use) parameter identifies the intended use of the public key. + /// The "use" parameter is employed to indicate whether a public key is used for encrypting + /// data or verifying the signature on data. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.2) + #[serde(alias = "use")] + pub _use: Option<String>, + /// The "key_ops" (key operations) parameter identifies the operation(s) for which the key + /// is intended to be used. The "key_ops" parameter is intended for use cases in which + /// public, private, or symmetric keys may be present. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.3) + pub key_ops: Vec<String>, + + /// The "alg" (algorithm) parameter identifies the algorithm intended for use with the key. + /// The values used should either be registered in the IANA "JSON Web Signature and + /// Encryption Algorithms" registry established by JWA or be a value that contains + /// a Collision-Resistant Name. The "alg" value is a case-sensitive ASCII string. + /// Use of this member is OPTIONAL. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.4) + pub alg: Option<String>, + + /// The "kid" (key ID) parameter is used to match a specific key. + /// This is used, for instance, to choose among a set of keys within a JWK Set during key + /// rollover. The structure of the "kid" value is unspecified. + /// When "kid" values are used within a JWK Set, different keys within the JWK Set SHOULD + /// use distinct "kid" values. (One example in which different keys might use the + /// same "kid" value is if they have different "kty" (key type) values but are considered + /// to be equivalent alternatives by the application using them.) + /// The "kid" value is a case-sensitive string. Use of this member is OPTIONAL. + /// When used with JWS or JWE, the "kid" value is used to match a JWS or JWE "kid" + /// Header Parameter value. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.5) + pub kid: Option<String>, + + /// The "x5u" (X.509 URL) parameter is a URI that refers to a resource for + /// an X.509 public key certificate or certificate chain + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.6) + pub x5u: Option<Url>, + + /// The "x5c" (X.509 certificate chain) parameter contains a chain of one or more + /// PKIX certificates [RFC5280](https://datatracker.ietf.org/doc/html/rfc5280). + /// The certificate chain is represented as a JSON array of certificate value strings. + /// Each string in the array is a base64-encoded (Section 4 of + /// [RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + /// -- not base64url-encoded) DER + /// [ITU.X690.1994](https://datatracker.ietf.org/doc/html/rfc7517#ref-ITU.X690.1994) + /// PKIX certificate value. The PKIX certificate containing the key value MUST be the first + /// certificate. This MAY be followed by additional certificates, with each subsequent + /// certificate being the one used to certify the previous one. The key in the first + /// certificate MUST match the public key represented by other members of the JWK. + /// Use of this member is OPTIONAL. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) + pub x5c: Option<String>, + + /// The "x5t" (X.509 certificate SHA-1 thumbprint) parameter is a base64url-encoded + /// SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate [RFC5280] + /// Note that certificate thumbprints are also sometimes known as certificate fingerprints. + /// The key in the certificate MUST match the public key represented by + /// other members of the JWK. Use of this member is OPTIONAL + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.8) + pub x5t: Option<String>, + + /// The "x5t#S256" (X.509 certificate SHA-256 thumbprint) parameter is a base64url-encoded + /// SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate Note that + /// certificate thumbprints are also sometimes known as certificate fingerprints. + /// The key in the certificate MUST match the public key represented by other members of + /// the JWK. Use of this member is OPTIONAL. + /// [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517#section-4.9) + #[serde(alias = "x5t#S256")] + pub x5t_s256: Option<String>, + + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, +} + +impl Hash for JsonWebKey { + fn hash<H: Hasher>(&self, state: &mut H) { + self.kty.hash(state); + self._use.hash(state); + } +} + +impl Display for JsonWebKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "kty: {}, use: {:#?}, key_ops: {:#?}, alg: {:#?}, kid: {:#?}, x5u: {:#?}, x5c: {:#?}, x5t: {:#?}, x5t#S256: {:#?}", + self.kty, self._use, self.key_ops, self.alg, self.kid, self.x5u, self.x5c, self.x5t, self.x5t_s256 ) + } +} + +/// A JSON Web Key Set (JWKS) is a JSON object that represents a set of JWKs. The JSON object MUST +/// have a "keys" member, which is an array of JWKs. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct JsonWebKeySet { + pub keys: HashSet<JsonWebKey>, +} + +impl Display for JsonWebKeySet { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "keys: {:#?}", self.keys) + } +} diff --git a/graph-core/src/crypto/mod.rs b/graph-core/src/crypto/mod.rs index 12037e47..051fa20c 100644 --- a/graph-core/src/crypto/mod.rs +++ b/graph-core/src/crypto/mod.rs @@ -1,5 +1,7 @@ +mod jwk; mod pkce; +pub use jwk::*; pub use pkce::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; diff --git a/graph-error/src/webview_error.rs b/graph-error/src/webview_error.rs index b8fc8d0f..0ab972bb 100644 --- a/graph-error/src/webview_error.rs +++ b/graph-error/src/webview_error.rs @@ -5,7 +5,7 @@ pub enum WebViewError { /// Webview Window closed for one of the following reasons: /// 1. The user closed the webview window without logging in. /// 2. The webview exited because of a timeout defined in the WebViewOptions. - #[error("WindowClosed: {0:#?}")] + #[error("window closed: {0:#?}")] WindowClosed(String), /// One of the following errors has occurred: @@ -25,7 +25,7 @@ pub enum WebViewError { /// The query or fragment of the redirect uri is an error returned /// from Microsoft. #[error("{error:#?}, {error_description:#?}, {error_uri:#?}")] - AuthorizationQuery { + Authorization { error: String, error_description: String, error_uri: Option<String>, @@ -46,11 +46,20 @@ pub enum WebViewDeviceCodeError { /// Webview Window closed for one of the following reasons: /// 1. The user closed the webview window without logging in. /// 2. The webview exited because of a timeout defined in the WebViewOptions. - #[error("window closed reason: {0:#?}")] + /// 3. The window or event loop was destroyed. The cause is unknown. + #[error("{0:#?}")] WindowClosed(String), /// Error that happens calling the http request. #[error("{0:#?}")] - AuthExecutionError(#[from] AuthExecutionError), + AuthExecutionError(#[from] Box<AuthExecutionError>), #[error("{0:#?}")] DeviceCodePollingError(http::Response<Result<serde_json::Value, ErrorMessage>>), } + +impl From<AuthorizationFailure> for WebViewDeviceCodeError { + fn from(value: AuthorizationFailure) -> Self { + WebViewDeviceCodeError::AuthExecutionError(Box::new(AuthExecutionError::Authorization( + value, + ))) + } +} diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index ee102200..088e2088 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -23,6 +23,7 @@ either = "1.9.0" dyn-clone = "1.0.14" hex = "0.4.3" http = "0.2.11" +jsonwebtoken = "9.1.0" lazy_static = "1.4.0" openssl = { version = "0.10", optional=true } reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 4ad838ca..591b8928 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -109,6 +109,18 @@ impl AzureCloudInstance { )) } + pub fn openid_configuration_uri(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!( + "{}/{}/v2.0/.well-known/openid-configuration", + self.as_ref(), + authority.as_ref() + )) + } + + pub fn issuer(&self, authority: &Authority) -> Result<Url, ParseError> { + Url::parse(&format!("{}/{}/v2.0", self.as_ref(), authority.as_ref())) + } + /* pub fn default_microsoft_graph_scope(&self) -> &'static str { "https://graph.microsoft.com/.default" diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index d8f1c27a..1abc973d 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,3 +1,5 @@ +use crate::identity::AppConfig; +use graph_error::{WebViewError, WebViewResult}; use serde::Deserializer; use serde_json::Value; use std::collections::HashMap; @@ -99,6 +101,33 @@ where Ok(None) } +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +pub(crate) struct PhantomAuthorizationResponse { + pub code: Option<String>, + pub id_token: Option<String>, + #[serde(default)] + #[serde(deserialize_with = "deserialize_expires_in")] + pub expires_in: Option<i64>, + pub access_token: Option<String>, + pub state: Option<String>, + pub session_state: Option<String>, + pub nonce: Option<String>, + pub error: Option<AuthorizationQueryError>, + pub error_description: Option<String>, + pub error_uri: Option<Url>, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, + #[serde(skip)] + log_pii: bool, +} + +#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct AuthorizationError { + pub error: Option<AuthorizationQueryError>, + pub error_description: Option<String>, + pub error_uri: Option<Url>, +} + #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AuthorizationResponse { pub code: Option<String>, @@ -164,6 +193,77 @@ impl Debug for AuthorizationResponse { } } +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub enum AuthorizationImpeded { + WindowClosed(String), + InvalidUri(String), +} + +#[derive(Clone, Debug)] +pub enum AuthorizationEvent<CredentialBuilder: Clone + Debug> { + Authorized { + authorization_response: AuthorizationResponse, + credential_builder: CredentialBuilder, + }, + Unauthorized(AuthorizationResponse), + WindowClosed(String), +} + +impl<CredentialBuilder: Clone + Debug> AuthorizationEvent<CredentialBuilder> { + pub fn into_result(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { + match self { + AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + } => Ok((authorization_response, credential_builder)), + AuthorizationEvent::Unauthorized(authorization_response) => { + Err(WebViewError::Authorization { + error: authorization_response + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: authorization_response.error_description.unwrap_or_default(), + error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), + }) + } + AuthorizationEvent::WindowClosed(reason) => Err(WebViewError::WindowClosed(reason)), + } + } +} + +pub trait IntoCredentialBuilder<CredentialBuilder: Clone + Debug> { + fn into_credential_builder(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)>; +} + +impl<CredentialBuilder: Clone + Debug> IntoCredentialBuilder<CredentialBuilder> + for WebViewResult<AuthorizationEvent<CredentialBuilder>> +{ + fn into_credential_builder(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { + match self { + Ok(auth_event) => match auth_event { + AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + } => Ok((authorization_response, credential_builder)), + AuthorizationEvent::Unauthorized(authorization_response) => { + Err(WebViewError::Authorization { + error: authorization_response + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: authorization_response + .error_description + .unwrap_or_default(), + error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), + }) + } + AuthorizationEvent::WindowClosed(reason) => Err(WebViewError::WindowClosed(reason)), + }, + Err(err) => Err(err), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index 4d72c7d9..a08b6853 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -178,7 +178,7 @@ impl ConfidentialClientApplicationBuilder { /// Auth Code Using X509 Certificate #[cfg(feature = "openssl")] - pub fn with_authorization_code_x509_certificate( + pub fn with_auth_code_x509_certificate( &mut self, authorization_code: impl AsRef<str>, x509: &X509Certificate, diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 96b83606..e846095c 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -8,12 +8,12 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; -use graph_error::{IdentityResult, AF}; +use graph_error::{AuthorizationFailure, IdentityResult, AF}; use crate::identity::{ - credentials::app_config::AppConfig, AsQuery, AuthorizationCodeAssertionCredentialBuilder, - AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, - ResponseType, + credentials::app_config::AppConfig, tracing_targets::INTERACTIVE_AUTH, AsQuery, + AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, + AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -31,6 +31,7 @@ use crate::web::{ HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, }; +use crate::oauth::AuthorizationEvent; #[cfg(feature = "interactive-auth")] use crate::web::UserEvents; #[cfg(feature = "interactive-auth")] @@ -229,7 +230,7 @@ impl AuthCodeAuthorizationUrlParameters { } #[cfg(feature = "interactive-auth")] - pub fn interactive_webview_authentication( + pub(crate) fn interactive_webview_authentication( &self, options: WebViewOptions, ) -> WebViewResult<AuthorizationResponse> { @@ -275,8 +276,8 @@ impl AuthCodeAuthorizationUrlParameters { .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; if response_query.is_err() { - tracing::debug!(target: "graph_rs_sdk::interactive_auth", "error in authorization query or fragment from redirect uri"); - return Err(WebViewError::AuthorizationQuery { + tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); + return Err(WebViewError::Authorization { error: response_query .error .map(|query_error| query_error.to_string()) @@ -286,7 +287,7 @@ impl AuthCodeAuthorizationUrlParameters { }); } - tracing::debug!(target: "graph_rs_sdk::interactive_auth", "parsed authorization query or fragment from redirect uri"); + tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); Ok(response_query) } @@ -296,43 +297,101 @@ impl AuthCodeAuthorizationUrlParameters { }, } } -} -#[cfg(feature = "interactive-auth")] -impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { - #[tracing::instrument] - fn webview( - host_options: HostOptions, - window: Window, - proxy: EventLoopProxy<UserEvents>, - ) -> anyhow::Result<WebView> { - let start_uri = host_options.start_uri.clone(); - let validator = WebViewHostValidator::try_from(host_options)?; - Ok(WebViewBuilder::new(window)? - .with_url(start_uri.as_ref())? - // Disables file drop - .with_file_drop_handler(|_, _| true) - .with_navigation_handler(move |uri| { - if let Ok(url) = Url::parse(uri.as_str()) { - let is_valid_host = validator.is_valid_uri(&url); - let is_redirect = validator.is_redirect_host(&url); - - if is_redirect { - proxy.send_event(UserEvents::ReachedRedirectUri(url)) - .unwrap(); - proxy.send_event(UserEvents::InternalCloseWindow) - .unwrap(); - return true; - } + #[cfg(feature = "interactive-auth")] + pub(crate) fn interactive_authentication_builder<CredentialBuilder: Clone>( + &self, + options: WebViewOptions, + ) -> WebViewResult<AuthorizationResponse> { + let uri = self + .url() + .map_err(|err| Box::new(AuthExecutionError::from(err)))?; + let redirect_uri = self.redirect_uri().cloned().unwrap(); + let (sender, receiver) = std::sync::mpsc::channel(); + + std::thread::spawn(move || { + AuthCodeAuthorizationUrlParameters::interactive_auth( + uri, + vec![redirect_uri], + options, + sender, + ) + .unwrap(); + }); + let mut iter = receiver.try_iter(); + let mut next = iter.next(); - is_valid_host - } else { - tracing::debug!(target: "graph_rs_sdk::interactive_auth", "unable to navigate webview - url is none"); - proxy.send_event(UserEvents::CloseWindow).unwrap(); - false + while next.is_none() { + next = iter.next(); + } + + match next { + None => unreachable!(), + Some(auth_event) => match auth_event { + InteractiveAuthEvent::InvalidRedirectUri(reason) => { + Err(WebViewError::InvalidUri(reason)) } - }) - .build()?) + InteractiveAuthEvent::ReachedRedirectUri(uri) => { + let query = uri + .query() + .or(uri.fragment()) + .ok_or(WebViewError::InvalidUri(format!( + "uri missing query or fragment: {}", + uri.to_string() + )))?; + + let response_query: AuthorizationResponse = + serde_urlencoded::from_str(query) + .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; + + Ok(response_query) + } + InteractiveAuthEvent::WindowClosed(window_close_reason) => { + Err(WebViewError::WindowClosed(window_close_reason.to_string())) + } + }, + } + } +} + +mod internal { + use super::*; + + #[cfg(feature = "interactive-auth")] + impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { + fn webview( + host_options: HostOptions, + window: Window, + proxy: EventLoopProxy<UserEvents>, + ) -> anyhow::Result<WebView> { + let start_uri = host_options.start_uri.clone(); + let validator = WebViewHostValidator::try_from(host_options)?; + Ok(WebViewBuilder::new(window)? + .with_url(start_uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + if let Ok(url) = Url::parse(uri.as_str()) { + let is_valid_host = validator.is_valid_uri(&url); + let is_redirect = validator.is_redirect_host(&url); + + if is_redirect { + proxy.send_event(UserEvents::ReachedRedirectUri(url)) + .unwrap(); + proxy.send_event(UserEvents::InternalCloseWindow) + .unwrap(); + return true; + } + + is_valid_host + } else { + tracing::debug!(target: INTERACTIVE_AUTH, "unable to navigate webview - url is none"); + proxy.send_event(UserEvents::CloseWindow).unwrap(); + false + } + }) + .build()?) + } } } @@ -502,8 +561,8 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } } - pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> &mut Self { - self.credential.app_config.redirect_uri = Some(redirect_uri.into_url().unwrap()); + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); self } @@ -612,94 +671,118 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } #[cfg(feature = "interactive-auth")] - pub fn with_interactive_authentication_for_secret( + pub fn with_interactive_auth_for_secret( &self, + client_secret: impl AsRef<str>, options: WebViewOptions, - ) -> WebViewResult<(AuthorizationResponse, AuthorizationCodeCredentialBuilder)> { - let query_response = self + ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeCredentialBuilder>> { + let authorization_response = self .credential .interactive_webview_authentication(options)?; - if let Some(authorization_code) = query_response.code.as_ref() { - Ok(( - query_response.clone(), + + if authorization_response.is_err() { + tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); + return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + } + + tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); + + let mut credential_builder = { + if let Some(authorization_code) = authorization_response.code.as_ref() { AuthorizationCodeCredentialBuilder::new_with_auth_code( authorization_code, self.credential.app_config.clone(), - ), - )) - } else { - Ok(( - query_response.clone(), + ) + } else { AuthorizationCodeCredentialBuilder::new_with_token( self.credential.app_config.clone(), - Token::from(query_response), - ), - )) - } + Token::from(authorization_response.clone()), + ) + } + }; + + credential_builder.with_client_secret(client_secret); + Ok(AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + }) } #[cfg(feature = "interactive-auth")] - pub fn with_interactive_authentication_for_assertion( + pub fn with_interactive_auth_for_assertion( &self, + client_assertion: impl AsRef<str>, options: WebViewOptions, - ) -> WebViewResult<( - AuthorizationResponse, - AuthorizationCodeAssertionCredentialBuilder, - )> { - let query_response = self + ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeAssertionCredentialBuilder>> { + let authorization_response = self .credential .interactive_webview_authentication(options)?; - if let Some(authorization_code) = query_response.code.as_ref() { - Ok(( - query_response.clone(), + + if authorization_response.is_err() { + tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); + return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + } + + tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); + let mut credential_builder = { + if let Some(authorization_code) = authorization_response.code.as_ref() { AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( self.credential.app_config.clone(), authorization_code, - ), - )) - } else { - Ok(( - query_response.clone(), + ) + } else { AuthorizationCodeAssertionCredentialBuilder::new_with_token( self.credential.app_config.clone(), - Token::from(query_response), - ), - )) - } + Token::from(authorization_response.clone()), + ) + } + }; + + credential_builder.with_client_assertion(client_assertion); + Ok(AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + }) } #[cfg(feature = "interactive-auth")] #[cfg(feature = "openssl")] - pub fn with_interactive_authentication_for_certificate( + pub fn with_interactive_auth_for_x509_certificate( &self, - options: WebViewOptions, x509: &X509Certificate, - ) -> WebViewResult<( - AuthorizationResponse, - AuthorizationCodeCertificateCredentialBuilder, - )> { - let query_response = self + options: WebViewOptions, + ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeCertificateCredentialBuilder>> { + let authorization_response = self .credential .interactive_webview_authentication(options)?; - if let Some(authorization_code) = query_response.code.as_ref() { - Ok(( - query_response.clone(), + + if authorization_response.is_err() { + tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); + return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + } + + tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); + let mut credential_builder = { + if let Some(authorization_code) = authorization_response.code.as_ref() { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( authorization_code, x509, self.credential.app_config.clone(), - )?, - )) - } else { - Ok(( - query_response.clone(), + )? + } else { AuthorizationCodeCertificateCredentialBuilder::new_with_token( - self.credential.app_config.clone(), - Token::from(query_response), + Token::from(authorization_response.clone()), x509, - )?, - )) - } + self.credential.app_config.clone(), + )? + } + }; + + credential_builder.with_x509(x509)?; + Ok(AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + }) } pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { @@ -714,7 +797,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self.credential.url() } - pub fn into_secret_credential_builder( + pub fn with_auth_code( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { @@ -724,7 +807,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { ) } - pub fn into_assertion_credential_builder( + pub fn with_auth_code_assertion( self, authorization_code: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { @@ -735,7 +818,7 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } #[cfg(feature = "openssl")] - pub fn into_certificate_credential_builder( + pub fn with_auth_code_x509_certificate( self, authorization_code: impl AsRef<str>, x509: &X509Certificate, diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index a3d02182..e7270823 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -4,6 +4,7 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; +use url::Url; use uuid::Uuid; @@ -422,9 +423,9 @@ impl AuthorizationCodeAssertionCredentialBuilder { self } - pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { - self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); - Ok(self) + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); + self } pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { @@ -449,3 +450,9 @@ impl AuthorizationCodeAssertionCredentialBuilder { self.credential } } + +impl Debug for AuthorizationCodeAssertionCredentialBuilder { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.credential.fmt(f) + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 23e2147d..07861f2c 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -4,19 +4,22 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; +use url::Url; use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; +use graph_error::{ + AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult, AF, +}; use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; use crate::identity::{ - AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, + AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -417,9 +420,9 @@ impl AuthorizationCodeCertificateCredentialBuilder { #[cfg(feature = "interactive-auth")] #[cfg(feature = "openssl")] pub(crate) fn new_with_token( - app_config: AppConfig, token: Token, x509: &X509Certificate, + app_config: AppConfig, ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { let cache_id = app_config.cache_id.clone(); let mut token_cache = InMemoryCacheStore::new(); @@ -441,6 +444,26 @@ impl AuthorizationCodeCertificateCredentialBuilder { Ok(builder) } + #[cfg(feature = "openssl")] + pub(crate) fn new_authorization_response( + value: (AppConfig, AuthorizationResponse, &X509Certificate), + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + let (app_config, authorization_response, x509) = value; + if let Some(authorization_code) = authorization_response.code.as_ref() { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + authorization_code, + x509, + app_config, + ) + } else { + AuthorizationCodeCertificateCredentialBuilder::new_with_token( + Token::from(authorization_response.clone()), + x509, + app_config, + ) + } + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self @@ -452,9 +475,9 @@ impl AuthorizationCodeCertificateCredentialBuilder { self } - pub fn with_redirect_uri(&mut self, redirect_uri: impl IntoUrl) -> anyhow::Result<&mut Self> { - self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); - Ok(self) + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); + self } pub fn with_code_verifier<T: AsRef<str>>(&mut self, code_verifier: T) -> &mut Self { @@ -510,3 +533,9 @@ impl From<AuthorizationCodeCertificateCredentialBuilder> builder.credential } } + +impl Debug for AuthorizationCodeCertificateCredentialBuilder { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.credential.fmt(f) + } +} diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 1fb4b31d..6a4c268a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -15,8 +15,8 @@ use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, - EXECUTOR_TRACING_TARGET, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance, + ConfidentialClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -69,6 +69,65 @@ impl Debug for AuthorizationCodeCredential { } impl AuthorizationCodeCredential { + pub fn new( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, + authorization_code: impl AsRef<str>, + ) -> IdentityResult<AuthorizationCodeCredential> { + Ok(AuthorizationCodeCredential { + app_config: AppConfig::builder(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .build(), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: client_secret.as_ref().to_owned(), + code_verifier: None, + token_cache: Default::default(), + }) + } + + pub fn new_with_redirect_uri( + tenant_id: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, + authorization_code: impl AsRef<str>, + redirect_uri: impl IntoUrl, + ) -> IdentityResult<AuthorizationCodeCredential> { + let redirect_uri_result = Url::parse(redirect_uri.as_str()); + let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; + + Ok(AuthorizationCodeCredential { + app_config: AppConfigBuilder::new(client_id.as_ref()) + .tenant(tenant_id.as_ref()) + .redirect_uri(redirect_uri) + .build(), + authorization_code: Some(authorization_code.as_ref().to_owned()), + refresh_token: None, + client_secret: client_secret.as_ref().to_owned(), + code_verifier: None, + token_cache: Default::default(), + }) + } + + pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { + self.refresh_token = Some(refresh_token.as_ref().to_owned()); + } + + pub fn builder<T: AsRef<str>, U: AsRef<str>>( + client_id: T, + client_secret: T, + authorization_code: U, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new(client_id, client_secret, authorization_code) + } + + pub fn authorization_url_builder( + client_id: impl TryInto<Uuid>, + ) -> AuthCodeAuthorizationUrlParameterBuilder { + AuthCodeAuthorizationUrlParameterBuilder::new(client_id) + } + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; @@ -123,7 +182,7 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some"); if let Ok(token) = self.execute_cached_token_refresh(cache_id.clone()) { return Ok(token); } @@ -131,23 +190,23 @@ impl TokenCache for AuthorizationCodeCredential { if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some"); if let Some(refresh_token) = token.refresh_token.as_ref() { self.refresh_token = Some(refresh_token.to_owned()); } self.execute_cached_token_refresh(cache_id) } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } ForceTokenRefresh::Once | ForceTokenRefresh::Always => { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); let token_result = self.execute_cached_token_refresh(cache_id); if self.app_config.force_token_refresh == ForceTokenRefresh::Once { self.app_config.force_token_refresh = ForceTokenRefresh::Never; @@ -166,7 +225,7 @@ impl TokenCache for AuthorizationCodeCredential { // Attempt to bypass a read on the token store by using previous // refresh token stored outside of RwLock if self.refresh_token.is_some() { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some"); if let Ok(token) = self .execute_cached_token_refresh_async(cache_id.clone()) .await @@ -180,14 +239,14 @@ impl TokenCache for AuthorizationCodeCredential { if let Some(refresh_token) = old_token.refresh_token.as_ref() { self.refresh_token = Some(refresh_token.to_owned()); } - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=Some"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=Some"); self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(old_token.clone()) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } } @@ -206,67 +265,6 @@ impl TokenCache for AuthorizationCodeCredential { } } -impl AuthorizationCodeCredential { - pub fn new( - tenant_id: impl AsRef<str>, - client_id: impl AsRef<str>, - client_secret: impl AsRef<str>, - authorization_code: impl AsRef<str>, - ) -> IdentityResult<AuthorizationCodeCredential> { - Ok(AuthorizationCodeCredential { - app_config: AppConfig::builder(client_id.as_ref()) - .tenant(tenant_id.as_ref()) - .build(), - authorization_code: Some(authorization_code.as_ref().to_owned()), - refresh_token: None, - client_secret: client_secret.as_ref().to_owned(), - code_verifier: None, - token_cache: Default::default(), - }) - } - - pub fn new_with_redirect_uri( - tenant_id: impl AsRef<str>, - client_id: impl AsRef<str>, - client_secret: impl AsRef<str>, - authorization_code: impl AsRef<str>, - redirect_uri: impl IntoUrl, - ) -> IdentityResult<AuthorizationCodeCredential> { - let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; - - Ok(AuthorizationCodeCredential { - app_config: AppConfigBuilder::new(client_id.as_ref()) - .tenant(tenant_id.as_ref()) - .redirect_uri(redirect_uri) - .build(), - authorization_code: Some(authorization_code.as_ref().to_owned()), - refresh_token: None, - client_secret: client_secret.as_ref().to_owned(), - code_verifier: None, - token_cache: Default::default(), - }) - } - - pub fn with_refresh_token<T: AsRef<str>>(&mut self, refresh_token: T) { - self.refresh_token = Some(refresh_token.as_ref().to_owned()); - } - - pub fn builder<T: AsRef<str>, U: AsRef<str>>( - client_id: T, - client_secret: T, - authorization_code: U, - ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new(client_id, client_secret, authorization_code) - } - - pub fn authorization_url_builder( - client_id: impl TryInto<Uuid>, - ) -> AuthCodeAuthorizationUrlParameterBuilder { - AuthCodeAuthorizationUrlParameterBuilder::new(client_id) - } -} - #[derive(Clone)] pub struct AuthorizationCodeCredentialBuilder { credential: AuthorizationCodeCredential, @@ -290,7 +288,6 @@ impl AuthorizationCodeCredentialBuilder { } } - #[cfg(feature = "interactive-auth")] pub(crate) fn new_with_token( app_config: AppConfig, token: Token, @@ -339,9 +336,9 @@ impl AuthorizationCodeCredentialBuilder { } /// Defaults to http://localhost - pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { - self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); - Ok(self) + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); + self } pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { @@ -492,6 +489,26 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { } } +impl Debug for AuthorizationCodeCredentialBuilder { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.credential.fmt(f) + } +} + +impl From<(AppConfig, AuthorizationResponse)> for AuthorizationCodeCredentialBuilder { + fn from(value: (AppConfig, AuthorizationResponse)) -> Self { + let (app_config, authorization_response) = value; + if let Some(authorization_code) = authorization_response.code.as_ref() { + AuthorizationCodeCredentialBuilder::new_with_auth_code(authorization_code, app_config) + } else { + AuthorizationCodeCredentialBuilder::new_with_token( + app_config, + Token::from(authorization_response.clone()), + ) + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 4fdae1f5..011e8461 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -14,8 +14,8 @@ use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, - CLIENT_ASSERTION_TYPE, EXECUTOR_TRACING_TARGET, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance, + ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; credential_builder!( @@ -55,9 +55,9 @@ pub struct ClientAssertionCredential { impl ClientAssertionCredential { pub fn new( - assertion: impl AsRef<str>, tenant_id: impl AsRef<str>, client_id: impl AsRef<str>, + assertion: impl AsRef<str>, ) -> ClientAssertionCredential { ClientAssertionCredential { app_config: AppConfig::builder(client_id.as_ref()) @@ -119,14 +119,14 @@ impl TokenCache for ClientAssertionCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } @@ -136,14 +136,14 @@ impl TokenCache for ClientAssertionCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token.clone()) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } } @@ -153,51 +153,6 @@ impl TokenCache for ClientAssertionCredential { } } -#[derive(Clone)] -pub struct ClientAssertionCredentialBuilder { - credential: ClientAssertionCredential, -} - -impl ClientAssertionCredentialBuilder { - pub fn new( - client_id: impl AsRef<str>, - signed_assertion: impl AsRef<str>, - ) -> ClientAssertionCredentialBuilder { - ClientAssertionCredentialBuilder { - credential: ClientAssertionCredential { - app_config: AppConfig::builder(client_id.as_ref()) - .scope(vec!["https://graph.microsoft.com/.default"]) - .build(), - client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), - client_assertion: signed_assertion.as_ref().to_owned(), - token_cache: Default::default(), - }, - } - } - - pub(crate) fn new_with_signed_assertion( - signed_assertion: impl AsRef<str>, - mut app_config: AppConfig, - ) -> ClientAssertionCredentialBuilder { - app_config - .scope - .insert("https://graph.microsoft.com/.default".to_string()); - ClientAssertionCredentialBuilder { - credential: ClientAssertionCredential { - app_config, - client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), - client_assertion: signed_assertion.as_ref().to_owned(), - token_cache: Default::default(), - }, - } - } - - pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { - self.credential.client_assertion = client_assertion.as_ref().to_owned(); - self - } -} - #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { @@ -249,3 +204,48 @@ impl TokenCredentialExecutor for ClientAssertionCredential { &self.app_config } } + +#[derive(Clone)] +pub struct ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential, +} + +impl ClientAssertionCredentialBuilder { + pub fn new( + client_id: impl AsRef<str>, + signed_assertion: impl AsRef<str>, + ) -> ClientAssertionCredentialBuilder { + ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential { + app_config: AppConfig::builder(client_id.as_ref()) + .scope(vec!["https://graph.microsoft.com/.default"]) + .build(), + client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), + client_assertion: signed_assertion.as_ref().to_owned(), + token_cache: Default::default(), + }, + } + } + + pub(crate) fn new_with_signed_assertion( + signed_assertion: impl AsRef<str>, + mut app_config: AppConfig, + ) -> ClientAssertionCredentialBuilder { + app_config + .scope + .insert("https://graph.microsoft.com/.default".to_string()); + ClientAssertionCredentialBuilder { + credential: ClientAssertionCredential { + app_config, + client_assertion_type: CLIENT_ASSERTION_TYPE.to_string(), + client_assertion: signed_assertion.as_ref().to_owned(), + token_cache: Default::default(), + }, + } + } + + pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + self.credential.client_assertion = client_assertion.as_ref().to_owned(); + self + } +} diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index db9f7a03..6ada20ed 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -15,8 +15,9 @@ use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; use crate::identity::{ - Authority, AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, - ConfidentialClientApplication, Token, TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance, + ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, + TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -44,7 +45,6 @@ credential_builder!( /// the certificate yourself. If you need to use your own assertion see /// [ClientAssertionCredential](crate::identity::ClientAssertionCredential) #[derive(Clone)] -#[allow(dead_code)] pub struct ClientCertificateCredential { pub(crate) app_config: AppConfig, /// The value must be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer. @@ -63,22 +63,11 @@ pub struct ClientCertificateCredential { } impl ClientCertificateCredential { - pub fn new<T: AsRef<str>>(client_id: T, client_assertion: T) -> ClientCertificateCredential { - ClientCertificateCredential { - app_config: AppConfig::builder(client_id.as_ref()) - .scope(vec!["https://graph.microsoft.com/.default"]) - .build(), - client_assertion_type: CLIENT_ASSERTION_TYPE.to_owned(), - client_assertion: client_assertion.as_ref().to_owned(), - token_cache: Default::default(), - } - } - #[cfg(feature = "openssl")] - pub fn x509<T: AsRef<str>>( + pub fn new<T: AsRef<str>>( client_id: T, x509: &X509Certificate, - ) -> anyhow::Result<ClientCertificateCredential> { + ) -> IdentityResult<ClientCertificateCredential> { let mut builder = ClientCertificateCredentialBuilder::new(client_id.as_ref()); builder.with_certificate(x509)?; Ok(builder.credential) @@ -143,14 +132,14 @@ impl TokenCache for ClientCertificateCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } @@ -160,14 +149,14 @@ impl TokenCache for ClientCertificateCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token refresh"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token refresh"); self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token.clone()) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request"); self.execute_cached_token_refresh_async(cache_id).await } } @@ -278,7 +267,7 @@ impl ClientCertificateCredentialBuilder { Ok(self) } - pub fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { + fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { self.credential.client_assertion = client_assertion.as_ref().to_owned(); self } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index 636aefb0..a2f18564 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -41,11 +41,14 @@ impl ClientCredentialsAuthorizationUrlParameters { ClientCredentialsAuthorizationUrlParameterBuilder::new(client_id) } - pub fn into_credential(self, client_secret: impl AsRef<str>) -> ClientSecretCredentialBuilder { + pub fn with_client_secret( + self, + client_secret: impl AsRef<str>, + ) -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new_with_client_secret(client_secret, self.app_config) } - pub fn into_assertion_credential( + pub fn with_client_assertion( self, signed_assertion: impl AsRef<str>, ) -> ClientAssertionCredentialBuilder { @@ -56,7 +59,7 @@ impl ClientCredentialsAuthorizationUrlParameters { } #[cfg(feature = "openssl")] - pub fn into_certificate_credential( + pub fn with_client_x509_certificate( self, _client_secret: impl AsRef<str>, x509: &X509Certificate, @@ -159,14 +162,17 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { self.credential.url() } - pub fn into_credential(self, client_secret: impl AsRef<str>) -> ClientSecretCredentialBuilder { + pub fn with_client_secret( + self, + client_secret: impl AsRef<str>, + ) -> ClientSecretCredentialBuilder { ClientSecretCredentialBuilder::new_with_client_secret( client_secret, self.credential.app_config, ) } - pub fn into_assertion_credential( + pub fn with_client_assertion( self, signed_assertion: impl AsRef<str>, ) -> ClientAssertionCredentialBuilder { @@ -177,7 +183,7 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { } #[cfg(feature = "openssl")] - pub fn into_certificate_credential( + pub fn with_client_x509_certificate( self, _client_secret: impl AsRef<str>, x509: &X509Certificate, diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index e76ec939..5821e3e1 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -12,9 +12,9 @@ use graph_core::identity::ForceTokenRefresh; use graph_error::{AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult}; use crate::identity::{ - credentials::app_config::AppConfig, Authority, AzureCloudInstance, - ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, - TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, + credentials::app_config::AppConfig, tracing_targets::CREDENTIAL_EXECUTOR, Authority, + AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, + ConfidentialClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -132,14 +132,14 @@ impl TokenCache for ClientSecretCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } @@ -148,14 +148,14 @@ impl TokenCache for ClientSecretCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token.clone()) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 4c0e1b31..5ebe0350 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -11,6 +11,12 @@ use tracing::error; use url::Url; use uuid::Uuid; +use crate::identity::{ + tracing_targets::INTERACTIVE_AUTH, AppConfig, Authority, AzureCloudInstance, + DeviceAuthorizationResponse, PollDeviceCodeEvent, PublicClientApplication, Token, + TokenCredentialExecutor, +}; +use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::http::{ AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, }; @@ -19,13 +25,6 @@ use graph_error::{ IdentityResult, }; -use crate::identity::credentials::app_config::AppConfig; -use crate::identity::{ - Authority, AzureCloudInstance, DeviceAuthorizationResponse, PollDeviceCodeEvent, - PublicClientApplication, Token, TokenCredentialExecutor, -}; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; - #[cfg(feature = "interactive-auth")] use graph_error::WebViewDeviceCodeError; @@ -453,7 +452,7 @@ impl DeviceCodePollingExecutor { Err(_) => { error!( target = "device_code_polling_executor", - "Invalid PollDeviceCodeEvent" + "invalid PollDeviceCodeEvent" ); break; } @@ -563,34 +562,43 @@ impl DeviceCodePollingExecutor { ) -> AuthExecutionResult<(DeviceAuthorizationResponse, DeviceCodeInteractiveAuth)> { let response = self.credential.execute()?; let device_authorization_response: DeviceAuthorizationResponse = response.json()?; + self.credential + .with_device_code(device_authorization_response.device_code.clone()); + Ok(( device_authorization_response.clone(), DeviceCodeInteractiveAuth { credential: self.credential.clone(), - device_authorization_response, + interval: Duration::from_secs(device_authorization_response.interval), + verification_uri: device_authorization_response.verification_uri.clone(), + verification_uri_complete: device_authorization_response + .verification_uri_complete + .clone(), }, )) } } -#[cfg(feature = "interactive-auth")] -impl InteractiveAuth for DeviceCodeCredential { - fn webview( - host_options: HostOptions, - window: Window, - _proxy: EventLoopProxy<UserEvents>, - ) -> anyhow::Result<WebView> { - Ok(WebViewBuilder::new(window)? - .with_url(host_options.start_uri.as_ref())? - // Disables file drop - .with_file_drop_handler(|_, _| true) - .with_navigation_handler(move |uri| { - if let Ok(url) = Url::parse(uri.as_str()) { - tracing::event!(tracing::Level::INFO, url = url.as_str()); - } - true - }) - .build()?) +pub(crate) mod internal { + use super::*; + + #[cfg(feature = "interactive-auth")] + impl InteractiveAuth for DeviceCodeCredential { + fn webview( + host_options: HostOptions, + window: Window, + _proxy: EventLoopProxy<UserEvents>, + ) -> anyhow::Result<WebView> { + Ok(WebViewBuilder::new(window)? + .with_url(host_options.start_uri.as_ref())? + // Disables file drop + .with_file_drop_handler(|_, _| true) + .with_navigation_handler(move |uri| { + tracing::debug!(target: INTERACTIVE_AUTH, url = uri.as_str()); + true + }) + .build()?) + } } } @@ -598,62 +606,55 @@ impl InteractiveAuth for DeviceCodeCredential { #[derive(Debug)] pub struct DeviceCodeInteractiveAuth { credential: DeviceCodeCredential, - pub device_authorization_response: DeviceAuthorizationResponse, + interval: Duration, + verification_uri: String, + verification_uri_complete: Option<String>, } #[allow(dead_code)] #[cfg(feature = "interactive-auth")] impl DeviceCodeInteractiveAuth { pub(crate) fn new( - credential: DeviceCodeCredential, + mut credential: DeviceCodeCredential, device_authorization_response: DeviceAuthorizationResponse, ) -> DeviceCodeInteractiveAuth { + credential.with_device_code(device_authorization_response.device_code.clone()); DeviceCodeInteractiveAuth { credential, - device_authorization_response, + interval: Duration::from_secs(device_authorization_response.interval), + verification_uri: device_authorization_response.verification_uri.clone(), + verification_uri_complete: device_authorization_response + .verification_uri_complete + .clone(), } } pub fn interactive_webview_authentication( &mut self, - options: Option<WebViewOptions>, + options: WebViewOptions, ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let url = { - if let Some(url_complete) = self - .device_authorization_response - .verification_uri_complete - .as_ref() - { - Url::parse(url_complete).unwrap() + if let Some(url_complete) = self.verification_uri_complete.as_ref() { + Url::parse(url_complete).map_err(AuthorizationFailure::from)? } else { - Url::parse(self.device_authorization_response.verification_uri.as_str()).unwrap() + Url::parse(self.verification_uri.as_str()).map_err(AuthorizationFailure::from)? } }; let (sender, _receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - DeviceCodeCredential::interactive_auth( - url, - vec![], - options.unwrap_or_default(), - sender, - ) - .unwrap(); + DeviceCodeCredential::interactive_auth(url, vec![], options, sender).unwrap(); }); self.poll() } - #[tracing::instrument] pub(crate) fn poll( &mut self, ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let mut credential = self.credential.clone(); - - let device_code = self.device_authorization_response.device_code.clone(); - let interval = Duration::from_secs(self.device_authorization_response.interval); - credential.with_device_code(device_code); + let interval = self.interval.clone(); let mut should_slow_down = false; @@ -667,13 +668,13 @@ impl DeviceCodeInteractiveAuth { } let response = credential.execute().unwrap(); - let http_response = response.into_http_response()?; + let http_response = response.into_http_response().map_err(|err| Box::new(err))?; let status = http_response.status(); if status.is_success() { let json = http_response.json().unwrap(); - let token: Token = - serde_json::from_value(json).map_err(AuthExecutionError::from)?; + let token: Token = serde_json::from_value(json) + .map_err(|err| Box::new(AuthExecutionError::from(err)))?; let cache_id = credential.app_config.cache_id.clone(); credential.token_cache.store(cache_id, token); return Ok(PublicClientApplication::from(credential)); diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 34bfe30e..ee10f15c 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,3 +1,4 @@ +pub use app_config::*; pub use application_builder::*; pub use as_query::*; pub use auth_code_authorization_url::*; @@ -58,4 +59,7 @@ mod token_credential_executor; #[cfg(feature = "openssl")] mod x509_certificate; -pub(crate) const EXECUTOR_TRACING_TARGET: &str = "graph_rs_sdk::credential_executor"; +pub(crate) mod tracing_targets { + pub const CREDENTIAL_EXECUTOR: &str = "graph_rs_sdk::credential_executor"; + pub const INTERACTIVE_AUTH: &str = "graph_rs_sdk::interactive_auth"; +} diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 7b968b15..f67a760a 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -11,8 +11,8 @@ use graph_error::{AuthorizationFailure, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, OpenIdCredentialBuilder, Prompt, - ResponseMode, ResponseType, + AsQuery, Authority, AuthorizationImpeded, AuthorizationUrl, AzureCloudInstance, + OpenIdCredentialBuilder, Prompt, ResponseMode, ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -27,6 +27,7 @@ use crate::web::{ HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, }; +use crate::oauth::{AuthorizationEvent, PhantomAuthorizationResponse}; #[cfg(feature = "interactive-auth")] use crate::web::UserEvents; #[cfg(feature = "interactive-auth")] @@ -196,16 +197,16 @@ impl OpenIdAuthorizationUrlParameters { /// who want to manually verify that the nonce stored in the client is the same as the /// nonce returned in the response from the authorization server. /// Verifying the nonce helps mitigate token replay attacks. - pub fn nonce(&mut self) -> &String { + pub fn nonce(&self) -> &String { &self.nonce } - #[tracing::instrument] #[cfg(feature = "interactive-auth")] pub fn interactive_webview_authentication( &self, + client_secret: impl AsRef<str>, web_view_options: WebViewOptions, - ) -> WebViewResult<AuthorizationResponse> { + ) -> WebViewResult<AuthorizationEvent<OpenIdCredentialBuilder>> { if self.response_mode.eq(&Some(ResponseMode::FormPost)) { return Err(AF::msg_err( "response_mode", @@ -247,29 +248,35 @@ impl OpenIdAuthorizationUrlParameters { uri.to_string() )))?; - let response_query: AuthorizationResponse = - serde_urlencoded::from_str(query) - .map_err(|err| WebViewError::InvalidUri(err.to_string()))?; + let authorization_response: AuthorizationResponse = + serde_urlencoded::from_str(query).map_err(|_| { + WebViewError::InvalidUri(format!( + "unable to deserialize query or fragment: {}", + uri.to_string() + )) + })?; - if response_query.is_err() { + if authorization_response.is_err() { tracing::debug!(target: "graph_rs_sdk::interactive_auth", "error in authorization query or fragment from redirect uri"); - return Err(WebViewError::AuthorizationQuery { - error: response_query - .error - .map(|query_error| query_error.to_string()) - .unwrap_or_default(), - error_description: response_query.error_description.unwrap_or_default(), - error_uri: response_query.error_uri.map(|uri| uri.to_string()), - }); + return Ok(AuthorizationEvent::Unauthorized(authorization_response)); } tracing::debug!(target: "graph_rs_sdk::interactive_auth", "parsed authorization query or fragment from redirect uri"); - Ok(response_query) - } - InteractiveAuthEvent::WindowClosed(window_close_reason) => { - Err(WebViewError::WindowClosed(window_close_reason.to_string())) + let mut credential_builder = OpenIdCredentialBuilder::from(( + self.app_config.clone(), + authorization_response.clone(), + )); + credential_builder.with_client_secret(client_secret); + + Ok(AuthorizationEvent::Authorized { + authorization_response, + credential_builder, + }) } + InteractiveAuthEvent::WindowClosed(window_close_reason) => Ok( + AuthorizationEvent::WindowClosed(window_close_reason.to_string()), + ), }, } } @@ -311,6 +318,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer.response_type(ResponseType::Code); } else { let response_types = self.response_type.as_query(); + dbg!(response_types.as_str()); if !RESPONSE_TYPES_SUPPORTED.contains(&response_types.as_str()) { return AuthorizationFailure::msg_result( "response_type", @@ -441,12 +449,9 @@ impl OpenIdAuthorizationUrlParameterBuilder { } } - pub fn with_redirect_uri<T: AsRef<str>>( - &mut self, - redirect_uri: T, - ) -> IdentityResult<&mut Self> { - self.credential.app_config.redirect_uri = Some(Url::parse(redirect_uri.as_ref())?); - Ok(self) + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); + self } pub fn with_client_id(&mut self, client_id: impl TryInto<Uuid>) -> &mut Self { @@ -557,30 +562,13 @@ impl OpenIdAuthorizationUrlParameterBuilder { } #[cfg(feature = "interactive-auth")] - pub fn with_interactive_authentication( + pub fn with_interactive_auth( &self, + client_secret: impl AsRef<str>, options: WebViewOptions, - ) -> WebViewResult<(AuthorizationResponse, OpenIdCredentialBuilder)> { - let query_response = self - .credential - .interactive_webview_authentication(options)?; - if let Some(authorization_code) = query_response.code.as_ref() { - Ok(( - query_response.clone(), - OpenIdCredentialBuilder::new_with_auth_code( - self.credential.app_config.clone(), - authorization_code, - ), - )) - } else { - Ok(( - query_response.clone(), - OpenIdCredentialBuilder::new_with_token( - self.credential.app_config.clone(), - Token::from(query_response.clone()), - ), - )) - } + ) -> WebViewResult<AuthorizationEvent<OpenIdCredentialBuilder>> { + self.credential + .interactive_webview_authentication(client_secret, options) } pub fn build(&self) -> OpenIdAuthorizationUrlParameters { @@ -595,8 +583,11 @@ impl OpenIdAuthorizationUrlParameterBuilder { self.credential.url() } - pub fn into_credential(self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { - OpenIdCredentialBuilder::new_with_auth_code(self.credential.app_config, authorization_code) + pub fn as_credential(&self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { + OpenIdCredentialBuilder::new_with_auth_code( + self.credential.app_config.clone(), + authorization_code, + ) } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index b9b39eb5..0c05dae9 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -5,21 +5,24 @@ use async_trait::async_trait; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; use reqwest::IntoUrl; -use url::Url; +use url::{ParseError, Url}; use uuid::Uuid; use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; +use graph_error::{ + AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult, AF, +}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - Authority, AzureCloudInstance, ConfidentialClientApplication, + Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, TokenCredentialExecutor, }; use crate::internal::{OAuthParameter, OAuthSerializer}; +use crate::oauth::JwtHeader; credential_builder!( OpenIdCredentialBuilder, @@ -61,7 +64,9 @@ pub struct OpenIdCredential { pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, + openid_config: Option<serde_json::Value>, } + impl Debug for OpenIdCredential { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("OpenIdCredential") @@ -90,6 +95,7 @@ impl OpenIdCredential { pkce: None, serializer: Default::default(), token_cache: Default::default(), + openid_config: None, }) } @@ -114,6 +120,84 @@ impl OpenIdCredential { self.pkce.as_ref() } + pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { + let uri = self + .app_config + .azure_cloud_instance + .openid_configuration_uri(&self.app_config.authority) + .map_err(AuthorizationFailure::from)?; + Ok(reqwest::blocking::get(uri)?) + } + + pub async fn get_openid_config_async(&self) -> AuthExecutionResult<reqwest::Response> { + let uri = self + .app_config + .azure_cloud_instance + .openid_configuration_uri(&self.app_config.authority) + .map_err(AuthorizationFailure::from)?; + reqwest::get(uri).await.map_err(AuthExecutionError::from) + } + + pub fn get_jwks(&self) -> AuthExecutionResult<reqwest::blocking::Response> { + let config_response = self.get_openid_config()?; + let json: serde_json::Value = config_response.json()?; + let jwks_uri = json["jwks_uri"] + .as_str() + .ok_or(AuthExecutionError::Authorization(AF::msg_err( + "jwks_uri", + "not found in openid configuration", + )))?; + Ok(reqwest::blocking::get(jwks_uri)?) + } + + pub async fn get_jwks_async(&self) -> AuthExecutionResult<reqwest::Response> { + let config_response = self.get_openid_config_async().await?; + let json: serde_json::Value = config_response.json().await?; + let jwks_uri = json["jwks_uri"] + .as_str() + .ok_or(AuthExecutionError::Authorization(AF::msg_err( + "jwks_uri", + "not found in openid configuration", + )))?; + reqwest::get(jwks_uri) + .await + .map_err(AuthExecutionError::from) + } + + pub fn get_jwks_key(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + let response = self.get_jwks()?; + let json: serde_json::Value = response.json()?; + let keys = json["keys"].as_array().ok_or(AF::msg_err( + "keys", + "required but not found in json web key set", + ))?; + keys.iter() + .find(|value| value["kid"].as_str().eq(&Some(kid))) + .cloned() + .ok_or(AF::msg_err("kid", "no match found in json web keys")) + .map_err(AuthExecutionError::from) + } + + pub async fn get_jwks_key_async(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + let response = self.get_jwks_async().await?; + let json: serde_json::Value = response.json().await?; + let keys = json["keys"].as_array().ok_or(AF::msg_err( + "keys", + "required but not found in json web key set", + ))?; + keys.iter() + .find(|value| value["kid"].as_str().eq(&Some(kid))) + .cloned() + .ok_or(AF::msg_err("kid", "no match found in json web keys")) + .map_err(AuthExecutionError::from) + } + + pub fn issuer(&self) -> Result<Url, ParseError> { + self.app_config + .azure_cloud_instance + .issuer(&self.app_config.authority) + } + fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; @@ -351,6 +435,12 @@ pub struct OpenIdCredentialBuilder { credential: OpenIdCredential, } +impl Debug for OpenIdCredentialBuilder { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.credential.fmt(f) + } +} + impl OpenIdCredentialBuilder { fn new(client_id: impl TryInto<Uuid>) -> OpenIdCredentialBuilder { Self { @@ -368,6 +458,7 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), + openid_config: None, }, } } @@ -384,6 +475,7 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), + openid_config: None, }, } } @@ -403,6 +495,7 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), + openid_config: None, }, } } @@ -423,11 +516,11 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), + openid_config: None, }, } } - #[cfg(feature = "interactive-auth")] pub(crate) fn new_with_token(app_config: AppConfig, token: Token) -> OpenIdCredentialBuilder { let cache_id = app_config.cache_id.clone(); let mut token_cache = InMemoryCacheStore::new(); @@ -443,6 +536,7 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache, + openid_config: None, }, } } @@ -488,6 +582,34 @@ impl OpenIdCredentialBuilder { Ok(self) } + pub fn issuer(&self) -> Result<Url, ParseError> { + self.credential.issuer() + } + + pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { + self.credential.get_openid_config() + } + + pub async fn get_openid_config_async(&self) -> AuthExecutionResult<reqwest::Response> { + self.credential.get_openid_config_async().await + } + + pub fn get_jwks(&self) -> AuthExecutionResult<reqwest::blocking::Response> { + self.credential.get_jwks() + } + + pub async fn get_jwks_async(&self) -> AuthExecutionResult<reqwest::Response> { + self.credential.get_jwks_async().await + } + + pub fn get_jwks_key(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + self.credential.get_jwks_key(kid) + } + + pub async fn get_jwks_key_async(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + self.credential.get_jwks_key_async(kid).await + } + pub fn credential(&self) -> &OpenIdCredential { &self.credential } @@ -505,6 +627,20 @@ impl From<OpenIdCredential> for OpenIdCredentialBuilder { } } +impl From<(AppConfig, AuthorizationResponse)> for OpenIdCredentialBuilder { + fn from(value: (AppConfig, AuthorizationResponse)) -> Self { + let (app_config, authorization_response) = value; + if let Some(authorization_code) = authorization_response.code.as_ref() { + OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code) + } else { + OpenIdCredentialBuilder::new_with_token( + app_config, + Token::from(authorization_response.clone()), + ) + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index e65a2cb1..6272f4ef 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -1,6 +1,7 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AzureCloudInstance, Token, TokenCredentialExecutor, EXECUTOR_TRACING_TARGET, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance, Token, + TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use async_trait::async_trait; @@ -117,14 +118,14 @@ impl TokenCache for ResourceOwnerPasswordCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh(cache_id) } } @@ -133,14 +134,14 @@ impl TokenCache for ResourceOwnerPasswordCredential { let cache_id = self.app_config.cache_id.to_string(); if let Some(token) = self.token_cache.get(cache_id.as_str()) { if token.is_expired_sub(time::Duration::minutes(5)) { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "using token from cache"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "using token from cache"); Ok(token.clone()) } } else { - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "executing silent token request; refresh_token=None"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "executing silent token request; refresh_token=None"); self.execute_cached_token_refresh_async(cache_id).await } } diff --git a/graph-oauth/src/identity/credentials/response_type.rs b/graph-oauth/src/identity/credentials/response_type.rs index 6a20bb40..e5adf59c 100644 --- a/graph-oauth/src/identity/credentials/response_type.rs +++ b/graph-oauth/src/identity/credentials/response_type.rs @@ -1,4 +1,5 @@ use std::collections::BTreeSet; +use std::fmt::Display; use crate::identity::AsQuery; @@ -6,19 +7,20 @@ use crate::identity::AsQuery; pub enum ResponseType { #[default] Code, - Token, IdToken, + Token, StringSet(BTreeSet<String>), } -impl ToString for ResponseType { - fn to_string(&self) -> String { - match self { +impl Display for ResponseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { ResponseType::Code => "code".to_owned(), - ResponseType::Token => "token".to_owned(), ResponseType::IdToken => "id_token".to_owned(), + ResponseType::Token => "token".to_owned(), ResponseType::StringSet(response_type_vec) => response_type_vec.iter().as_query(), - } + }; + write!(f, "{}", str) } } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index d097368a..04fdefa5 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -12,7 +12,7 @@ use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - Authority, AuthorizationRequestParts, AzureCloudInstance, EXECUTOR_TRACING_TARGET, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationRequestParts, AzureCloudInstance, }; dyn_clone::clone_trait_object!(TokenCredentialExecutor); @@ -56,7 +56,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: EXECUTOR_TRACING_TARGET, + target: CREDENTIAL_EXECUTOR, "authorization request constructed" ); Ok(request_builder) @@ -67,7 +67,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: EXECUTOR_TRACING_TARGET, + target: CREDENTIAL_EXECUTOR, "authorization request constructed" ); Ok(request_builder) @@ -91,7 +91,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: EXECUTOR_TRACING_TARGET, + target: CREDENTIAL_EXECUTOR, "authorization request constructed" ); Ok(request_builder) @@ -102,7 +102,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { .form(&auth_request.form_urlencoded); tracing::debug!( - target: EXECUTOR_TRACING_TARGET, + target: CREDENTIAL_EXECUTOR, "authorization request constructed" ); Ok(request_builder) @@ -139,7 +139,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { let request_builder = self.build_request()?; let response = request_builder.send()?; let status = response.status(); - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "authorization response received; status={status:#?}"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "authorization response received; status={status:#?}"); Ok(response) } @@ -147,7 +147,7 @@ pub trait TokenCredentialExecutor: DynClone + Debug { let request_builder = self.build_request_async()?; let response = request_builder.send().await?; let status = response.status(); - tracing::debug!(target: EXECUTOR_TRACING_TARGET, "authorization response received; status={status:#?}"); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "authorization response received; status={status:#?}"); Ok(response) } } diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 434ec51c..06442f71 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -54,7 +54,7 @@ pub struct X509Certificate { } impl X509Certificate { - pub fn new<T: AsRef<str>>(client_id: T, certificate: X509, private_key: PKey<Private>) -> Self { + pub fn new(client_id: impl AsRef<str>, certificate: X509, private_key: PKey<Private>) -> Self { Self { client_id: client_id.as_ref().to_owned(), tenant_id: None, @@ -68,9 +68,9 @@ impl X509Certificate { } } - pub fn new_with_tenant<T: AsRef<str>>( - client_id: T, - tenant_id: T, + pub fn new_with_tenant( + client_id: impl AsRef<str>, + tenant_id: impl AsRef<str>, certificate: X509, private_key: PKey<Private>, ) -> Self { @@ -87,9 +87,9 @@ impl X509Certificate { } } - pub fn new_from_pass<T: AsRef<str>>( - client_id: T, - pass: T, + pub fn new_from_pass( + client_id: impl AsRef<str>, + pass: impl AsRef<str>, certificate: X509, ) -> IdentityResult<Self> { let der = encode_cert(&certificate)?; @@ -123,10 +123,10 @@ impl X509Certificate { }) } - pub fn new_from_pass_with_tenant<T: AsRef<str>>( - client_id: T, - tenant_id: T, - pass: T, + pub fn new_from_pass_with_tenant( + client_id: impl AsRef<str>, + tenant_id: impl AsRef<str>, + pass: impl AsRef<str>, certificate: X509, ) -> IdentityResult<Self> { let der = encode_cert(&certificate)?; diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index c8e60a0b..ca436cad 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -4,11 +4,56 @@ use serde::{Deserialize, Deserializer}; use serde_json::Value; use std::collections::HashMap; use std::convert::TryFrom; -use std::fmt::{Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; +use base64::{DecodeError, Engine}; +use jsonwebtoken::errors as JwtErrors; +use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation}; use std::str::FromStr; use url::form_urlencoded::parse; +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct JwtHeader { + pub typ: String, + pub alg: String, + pub kid: String, + pub x5t: Option<String>, +} + +impl Display for JwtHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "typ: {}, alg: {}, kid: {}, x5t: {:#?}", + self.typ, self.alg, self.kid, self.x5t + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Claims { + pub aud: String, + pub iss: String, + pub iat: usize, + pub nbf: usize, + pub exp: usize, + pub aio: String, + pub c_hash: String, + pub cc: String, + pub email: String, + pub name: String, + pub nonce: String, + pub oid: String, + pub preferred_username: String, + pub rh: String, + pub sub: String, + pub tid: String, + pub uti: String, + pub ver: String, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, +} + /// ID tokens are sent to the client application as part of an OpenID Connect flow. /// They can be sent alongside or instead of an access token. ID tokens are used by the /// client to authenticate the user. To learn more about how the Microsoft identity @@ -26,17 +71,72 @@ pub struct IdToken { } impl IdToken { - pub fn new(id_token: &str, code: &str, state: &str, session_state: &str) -> IdToken { + pub fn new( + id_token: &str, + code: Option<&str>, + state: Option<&str>, + session_state: Option<&str>, + ) -> IdToken { IdToken { - code: Some(code.into()), + code: code.map(|value| value.into()), id_token: id_token.into(), - state: Some(state.into()), - session_state: Some(session_state.into()), + state: state.map(|value| value.into()), + session_state: session_state.map(|value| value.into()), additional_fields: Default::default(), log_pii: false, } } + /// Decode the id token payload. + pub fn decode_payload(&self) -> JwtErrors::Result<serde_json::Value> { + let parts: Vec<&str> = self.id_token.split('.').collect(); + if parts.is_empty() { + return Err(JwtErrors::Error::from(JwtErrors::ErrorKind::InvalidToken)); + } + let payload_decoded = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(parts[1]) + .unwrap(); + let utf8_payload = String::from_utf8(payload_decoded)?.to_owned(); + let payload: serde_json::Value = serde_json::from_str(&utf8_payload)?; + Ok(payload) + } + + /// Decode the id token header. + pub fn decode_header(&self) -> JwtErrors::Result<jsonwebtoken::Header> { + /* + let parts: Vec<&str> = self.id_token.split('.').collect(); + if parts.is_empty() { + return Err(JwtErrors::Error::from(JwtErrors::ErrorKind::InvalidToken)); + } + let header_decoded = base64::engine::general_purpose::STANDARD_NO_PAD.decode(parts[0])?; + let utf8_header = String::from_utf8(header_decoded)?; + let jwt_header: JwtHeader = serde_json::from_str(&utf8_header)?; + */ + + jsonwebtoken::decode_header(self.id_token.as_str()) + } + + /// Decode and validate the id token. + pub fn decode( + &self, + n: &str, + e: &str, + client_id: &str, + issuer: Option<&str>, + ) -> JwtErrors::Result<TokenData<Claims>> { + let mut validation = Validation::new(Algorithm::RS256); + validation.set_audience(&[client_id]); + if let Some(issuer) = issuer { + validation.set_issuer(&[issuer]); + } + + jsonwebtoken::decode::<Claims>( + &self.id_token, + &DecodingKey::from_rsa_components(n, e).unwrap(), + &validation, + ) + } + /// Enable or disable logging of personally identifiable information such /// as logging the id_token. This is disabled by default. When log_pii is enabled /// passing an [IdToken] to logging or print functions will log id_token field. @@ -46,6 +146,22 @@ impl IdToken { } } +impl Display for IdToken { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:#?}, {:#?}, {:#?}, {:#?}", + self.id_token, self.state, self.session_state, self.code + ) + } +} + +impl AsRef<str> for IdToken { + fn as_ref(&self) -> &str { + self.id_token.as_str() + } +} + impl TryFrom<String> for IdToken { type Error = serde::de::value::Error; @@ -146,6 +262,10 @@ impl FromStr for IdToken { type Err = serde::de::value::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { - serde_urlencoded::from_str(s) + let deserialize_result = serde_urlencoded::from_str(s); + if deserialize_result.is_err() { + return Ok(IdToken::new(s, None, None, None)); + } + deserialize_result } } diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 1c62dcc4..86bb33ed 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -6,8 +6,9 @@ use std::collections::HashMap; use std::fmt; use std::ops::{Add, Sub}; -use crate::identity::{AuthorizationResponse, IdToken}; +use crate::identity::{Authority, AuthorizationResponse, Claims, IdToken}; use graph_core::cache::AsBearer; +use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation}; use std::str::FromStr; use time::OffsetDateTime; @@ -107,7 +108,7 @@ pub struct Token { /// [Refresh tokens in the Microsoft identity platform.](https://learn.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens) pub refresh_token: Option<String>, pub user_id: Option<String>, - pub id_token: Option<String>, + pub id_token: Option<IdToken>, pub state: Option<String>, pub correlation_id: Option<String>, pub client_info: Option<String>, @@ -246,7 +247,7 @@ impl Token { /// access_token.set_id_token("id_token"); /// ``` pub fn set_id_token(&mut self, s: &str) -> &mut Self { - self.id_token = Some(s.to_string()); + self.id_token = Some(IdToken::new(s, None, None, None)); self } @@ -260,11 +261,7 @@ impl Token { /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); /// ``` pub fn with_id_token(&mut self, id_token: IdToken) { - self.id_token = Some(id_token.id_token); - } - - pub fn parse_id_token(&mut self) -> Option<Result<IdToken, serde::de::value::Error>> { - self.id_token.clone().map(|s| IdToken::from_str(s.as_str())) + self.id_token = Some(id_token); } /// Set the state. @@ -377,6 +374,41 @@ impl Token { pub fn elapsed(&self) -> Option<time::Duration> { Some(self.expires_on? - self.timestamp?) } + + pub fn decode_header(&self) -> jsonwebtoken::errors::Result<jsonwebtoken::Header> { + let id_token = self + .id_token + .as_ref() + .ok_or(jsonwebtoken::errors::Error::from( + jsonwebtoken::errors::ErrorKind::InvalidToken, + ))?; + jsonwebtoken::decode_header(id_token.as_ref()) + } + + /// Decode and validate the id token. + pub fn decode( + &self, + n: &str, + e: &str, + client_id: &str, + issuer: &str, + ) -> jsonwebtoken::errors::Result<TokenData<Claims>> { + let id_token = self + .id_token + .as_ref() + .ok_or(jsonwebtoken::errors::Error::from( + jsonwebtoken::errors::ErrorKind::InvalidToken, + ))?; + let mut validation = Validation::new(Algorithm::RS256); + validation.set_audience(&[client_id]); + validation.set_issuer(&[issuer]); + + jsonwebtoken::decode::<Claims>( + id_token.as_ref(), + &DecodingKey::from_rsa_components(n, e).unwrap(), + &validation, + ) + } } impl Default for Token { @@ -413,7 +445,9 @@ impl From<AuthorizationResponse> for Token { scope: vec![], refresh_token: None, user_id: None, - id_token: value.id_token, + id_token: value + .id_token + .map(|id_token| IdToken::new(id_token.as_ref(), None, None, None)), state: None, correlation_id: None, client_info: None, @@ -532,6 +566,14 @@ impl<'de> Deserialize<'de> for Token { let timestamp = OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); + let id_token = { + if let Some(id_token_string) = phantom_access_token.id_token.as_ref() { + IdToken::from_str(id_token_string.as_ref()).ok() + } else { + None + } + }; + Ok(Token { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, @@ -540,7 +582,7 @@ impl<'de> Deserialize<'de> for Token { scope: phantom_access_token.scope, refresh_token: phantom_access_token.refresh_token, user_id: phantom_access_token.user_id, - id_token: phantom_access_token.id_token, + id_token, state: phantom_access_token.state, correlation_id: phantom_access_token.correlation_id, client_info: phantom_access_token.client_info, diff --git a/graph-oauth/src/web/interactive_auth.rs b/graph-oauth/src/web/interactive_auth.rs index 178aad7d..19e91c7e 100644 --- a/graph-oauth/src/web/interactive_auth.rs +++ b/graph-oauth/src/web/interactive_auth.rs @@ -1,5 +1,5 @@ +use crate::identity::tracing_targets::INTERACTIVE_AUTH; use crate::web::{HostOptions, WebViewOptions}; -use graph_error::WebViewResult; use std::fmt::{Debug, Display, Formatter}; use std::sync::mpsc::Sender; use std::time::{Duration, Instant}; @@ -22,6 +22,7 @@ pub enum WindowCloseReason { start: Instant, requested_resume: Instant, }, + WindowDestroyed, } impl Display for WindowCloseReason { @@ -29,6 +30,7 @@ impl Display for WindowCloseReason { match self { WindowCloseReason::CloseRequested => write!(f, "CloseRequested"), WindowCloseReason::TimedOut { .. } => write!(f, "TimedOut"), + WindowCloseReason::WindowDestroyed => write!(f, "WindowDestroyed"), } } } @@ -47,13 +49,6 @@ pub enum UserEvents { ReachedRedirectUri(Url), } -pub trait InteractiveAuthenticator { - fn interactive_authentication( - &self, - interactive_web_view_options: Option<WebViewOptions>, - ) -> WebViewResult<std::sync::mpsc::Receiver<InteractiveAuthEvent>>; -} - pub trait InteractiveAuth where Self: Debug, @@ -84,12 +79,23 @@ where } match event { - Event::NewEvents(StartCause::Init) => tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Webview runtime started"), - Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume, .. }) => { - sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::TimedOut { - start, requested_resume - })).unwrap_or_default(); - tracing::debug!(target: "graph_rs_sdk::interactive_auth", "Timeout reached - closing window"); + Event::NewEvents(StartCause::Init) => { + tracing::debug!(target: INTERACTIVE_AUTH, "webview runtime started") + } + Event::NewEvents(StartCause::ResumeTimeReached { + start, + requested_resume, + .. + }) => { + sender + .send(InteractiveAuthEvent::WindowClosed( + WindowCloseReason::TimedOut { + start, + requested_resume, + }, + )) + .unwrap_or_default(); + tracing::debug!(target: INTERACTIVE_AUTH, "timeout reached - closing window"); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); @@ -99,12 +105,33 @@ where std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } - Event::UserEvent(UserEvents::CloseWindow) | Event::WindowEvent { + Event::LoopDestroyed + | Event::WindowEvent { + event: WindowEvent::Destroyed, + .. + } => { + tracing::debug!(target: INTERACTIVE_AUTH, "window destroyed"); + sender + .send(InteractiveAuthEvent::WindowClosed( + WindowCloseReason::WindowDestroyed, + )) + .unwrap_or_default(); + + // Wait time to avoid deadlock where window closes before receiver gets the event + std::thread::sleep(Duration::from_millis(500)); + *control_flow = ControlFlow::Exit + } + Event::UserEvent(UserEvents::CloseWindow) + | Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { - sender.send(InteractiveAuthEvent::WindowClosed(WindowCloseReason::CloseRequested)).unwrap_or_default(); - tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Window close requested by user"); + tracing::debug!(target: INTERACTIVE_AUTH, "window close requested by user"); + sender + .send(InteractiveAuthEvent::WindowClosed( + WindowCloseReason::CloseRequested, + )) + .unwrap_or_default(); if options.clear_browsing_data { let _ = webview.clear_all_browsing_data(); @@ -115,18 +142,21 @@ where *control_flow = ControlFlow::Exit } Event::UserEvent(UserEvents::ReachedRedirectUri(uri)) => { - tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Matched on redirect uri: {uri}"); - sender.send(InteractiveAuthEvent::ReachedRedirectUri(uri)) + tracing::debug!(target: INTERACTIVE_AUTH, "matched on redirect uri: {uri}"); + sender + .send(InteractiveAuthEvent::ReachedRedirectUri(uri)) .unwrap_or_default(); } Event::UserEvent(UserEvents::InternalCloseWindow) => { - tracing::trace!(target: "graph_rs_sdk::interactive_auth", "Closing window"); + tracing::debug!(target: INTERACTIVE_AUTH, "closing window"); if options.clear_browsing_data { + tracing::debug!(target: INTERACTIVE_AUTH, "clearing browsing data"); let _ = webview.clear_all_browsing_data(); } // Wait time to avoid deadlock where window closes before - // the channel has received the redirect uri. + // the channel has received the redirect uri. InternalCloseWindow + // is called after ReachedRedirectUri. std::thread::sleep(Duration::from_millis(500)); *control_flow = ControlFlow::Exit } diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs index c1eb141e..d4aa9e73 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/web/mod.rs @@ -1,6 +1,7 @@ -mod interactive_authenticator; +mod interactive_auth; mod webview_host_validator; mod webview_options; -pub use interactive_authenticator::*; +pub use interactive_auth::*; +pub use webview_host_validator::*; pub use webview_options::*; From 6ebd88a2ae3025b8d12ce2937ad7b52a01eeb33f Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 24 Dec 2023 07:03:42 -0500 Subject: [PATCH 073/118] Begin renmaing modules for graph-oauth::oauth and IdentityApiClient in prep for renaming to graph-identity --- Cargo.toml | 4 +- README.md | 6 +- .../README.md | 0 .../auth_code_grant.rs | 5 +- .../client_credentials.rs | 2 +- .../legacy/implicit_grant.rs | 4 +- .../legacy/mod.rs | 0 .../main.rs | 2 +- .../openid_connect.rs | 2 +- .../auth_code_grant/auth_code_certificate.rs | 36 +-- .../auth_code_grant/interactive_auth.rs | 51 ++++ .../auth_code_grant/mod.rs | 1 + .../auth_code_grant/server_example/mod.rs | 9 +- .../client_credentials_certificate.rs | 2 +- .../client_credentials/mod.rs | 0 .../main.rs | 0 .../README.md | 0 .../auth_code_grant/auth_code_grant_pkce.rs | 2 +- .../auth_code_grant/auth_code_grant_secret.rs | 8 +- .../auth_code_grant/mod.rs | 0 .../server_examples/auth_code_grant_pkce.rs | 2 +- .../server_examples/auth_code_grant_secret.rs | 2 +- .../auth_code_grant/server_examples/mod.rs | 0 .../client_credentials_secret.rs | 2 +- .../client_credentials/mod.rs | 2 +- .../client_credentials_admin_consent.rs | 2 +- .../client_credentials/server_examples/mod.rs | 0 .../device_code.rs | 9 +- .../environment_credential.rs | 2 +- .../getting_tokens_manually.rs | 2 +- .../is_access_token_expired.rs | 2 +- .../{oauth => identity_platform_auth}/main.rs | 2 +- .../openid/mod.rs | 0 .../openid/openid.rs | 2 +- .../openid/server_examples/mod.rs | 0 .../openid/server_examples/openid.rs | 2 +- examples/interactive_auth/INTERACTIVE_AUTH.md | 6 +- examples/interactive_auth/auth_code.rs | 11 +- examples/interactive_auth/openid.rs | 2 +- examples/interactive_auth/webview_options.rs | 27 +- graph-core/Cargo.toml | 1 + graph-core/src/cache/token_cache.rs | 6 +- graph-core/src/crypto/mod.rs | 2 - graph-core/src/identity/client_application.rs | 5 + graph-core/src/{crypto => identity}/jwk.rs | 0 graph-core/src/identity/jwks.rs | 72 +++++ graph-core/src/identity/mod.rs | 4 + graph-error/Cargo.toml | 1 + graph-error/src/authorization_failure.rs | 3 + graph-error/src/graph_failure.rs | 4 + graph-http/src/client.rs | 6 +- graph-oauth/Cargo.toml | 1 + .../src/identity/application_options.rs | 9 +- .../identity/authorization_query_response.rs | 37 ++- .../src/identity/credentials/app_config.rs | 11 +- .../credentials/application_builder.rs | 2 +- .../auth_code_authorization_url.rs | 193 +++++++------ ...authorization_code_assertion_credential.rs | 2 +- ...thorization_code_certificate_credential.rs | 2 +- .../authorization_code_credential.rs | 45 ++- .../client_assertion_credential.rs | 18 +- .../client_credentials_authorization_url.rs | 2 +- .../credentials/client_secret_credential.rs | 2 +- .../confidential_client_application.rs | 43 +-- graph-oauth/src/identity/credentials/mod.rs | 16 ++ .../credentials/open_id_authorization_url.rs | 46 ++- .../credentials/open_id_credential.rs | 272 +++++++++++++++--- .../credentials/token_credential_executor.rs | 7 +- graph-oauth/src/identity/id_token.rs | 104 +++---- graph-oauth/src/identity/mod.rs | 2 - graph-oauth/src/identity/token.rs | 102 +++++-- graph-oauth/src/identity/token_validator.rs | 16 -- graph-oauth/src/lib.rs | 56 ++-- graph-oauth/src/web/interactive_auth.rs | 23 ++ graph-oauth/src/web/webview_options.rs | 16 +- src/client/graph.rs | 18 +- src/{identity => identity_access}/mod.rs | 0 src/{identity => identity_access}/request.rs | 0 src/lib.rs | 22 +- test-tools/src/oauth_request.rs | 2 +- 80 files changed, 910 insertions(+), 474 deletions(-) rename examples/{oauth_authorization_url => authorization_sign_in}/README.md (100%) rename examples/{oauth_authorization_url => authorization_sign_in}/auth_code_grant.rs (93%) rename examples/{oauth_authorization_url => authorization_sign_in}/client_credentials.rs (90%) rename examples/{oauth_authorization_url => authorization_sign_in}/legacy/implicit_grant.rs (94%) rename examples/{oauth_authorization_url => authorization_sign_in}/legacy/mod.rs (100%) rename examples/{oauth_authorization_url => authorization_sign_in}/main.rs (98%) rename examples/{oauth_authorization_url => authorization_sign_in}/openid_connect.rs (99%) rename examples/{oauth_certificate => certificate_auth}/auth_code_grant/auth_code_certificate.rs (63%) create mode 100644 examples/certificate_auth/auth_code_grant/interactive_auth.rs rename examples/{oauth_certificate => certificate_auth}/auth_code_grant/mod.rs (68%) rename examples/{oauth_certificate => certificate_auth}/auth_code_grant/server_example/mod.rs (95%) rename examples/{oauth_certificate => certificate_auth}/client_credentials/client_credentials_certificate.rs (98%) rename examples/{oauth_certificate => certificate_auth}/client_credentials/mod.rs (100%) rename examples/{oauth_certificate => certificate_auth}/main.rs (100%) rename examples/{oauth => identity_platform_auth}/README.md (100%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/auth_code_grant_pkce.rs (98%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/auth_code_grant_secret.rs (87%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/mod.rs (100%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/server_examples/auth_code_grant_pkce.rs (99%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/server_examples/auth_code_grant_secret.rs (99%) rename examples/{oauth => identity_platform_auth}/auth_code_grant/server_examples/mod.rs (100%) rename examples/{oauth => identity_platform_auth}/client_credentials/client_credentials_secret.rs (90%) rename examples/{oauth => identity_platform_auth}/client_credentials/mod.rs (93%) rename examples/{oauth => identity_platform_auth}/client_credentials/server_examples/client_credentials_admin_consent.rs (98%) rename examples/{oauth => identity_platform_auth}/client_credentials/server_examples/mod.rs (100%) rename examples/{oauth => identity_platform_auth}/device_code.rs (84%) rename examples/{oauth => identity_platform_auth}/environment_credential.rs (95%) rename examples/{oauth => identity_platform_auth}/getting_tokens_manually.rs (98%) rename examples/{oauth => identity_platform_auth}/is_access_token_expired.rs (91%) rename examples/{oauth => identity_platform_auth}/main.rs (98%) rename examples/{oauth => identity_platform_auth}/openid/mod.rs (100%) rename examples/{oauth => identity_platform_auth}/openid/openid.rs (89%) rename examples/{oauth => identity_platform_auth}/openid/server_examples/mod.rs (100%) rename examples/{oauth => identity_platform_auth}/openid/server_examples/openid.rs (99%) rename graph-core/src/{crypto => identity}/jwk.rs (100%) create mode 100644 graph-core/src/identity/jwks.rs delete mode 100644 graph-oauth/src/identity/token_validator.rs rename src/{identity => identity_access}/mod.rs (100%) rename src/{identity => identity_access}/request.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 882431d2..6ebe1d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,8 +79,8 @@ debug = false [[example]] name = "oauth_certificate" -path = "examples/oauth_certificate/main.rs" -required-features = ["openssl"] +path = "examples/certificate_auth/main.rs" +required-features = ["interactive-auth", "openssl"] [[example]] name = "interactive_auth" diff --git a/README.md b/README.md index be921569..2383481e 100644 --- a/README.md +++ b/README.md @@ -1055,7 +1055,7 @@ async fn build_client( #### Authorization Code Secret With Proof Key Code Exchange ```rust -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, ProofKeyCodeExchange, TokenCredentialExecutor, }; @@ -1225,7 +1225,7 @@ Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) cra platforms that support it such as on a desktop. ```rust -use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; +use graph_rs_sdk::{identity::{AuthorizationCodeCredential, Secret}, GraphClient}; async fn authenticate( tenant_id: &str, @@ -1242,7 +1242,7 @@ async fn authenticate( .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(Default::default()) + .with_interactive_auth(Secret("client-secret".to_string()), Default::default()) .unwrap(); debug!("{authorization_query_response:#?}"); diff --git a/examples/oauth_authorization_url/README.md b/examples/authorization_sign_in/README.md similarity index 100% rename from examples/oauth_authorization_url/README.md rename to examples/authorization_sign_in/README.md diff --git a/examples/oauth_authorization_url/auth_code_grant.rs b/examples/authorization_sign_in/auth_code_grant.rs similarity index 93% rename from examples/oauth_authorization_url/auth_code_grant.rs rename to examples/authorization_sign_in/auth_code_grant.rs index 99ac5dcc..977a6f75 100644 --- a/examples/oauth_authorization_url/auth_code_grant.rs +++ b/examples/authorization_sign_in/auth_code_grant.rs @@ -1,12 +1,13 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, Token, TokenCredentialExecutor, }; +use url::Url; static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static REDIRECT_URI: Url = Url::parse("http://localhost:8000/redirect").unwrap(); static SCOPE: &str = "User.Read"; // or pass more values to vec![] below // Authorization Code Grant Auth URL Builder diff --git a/examples/oauth_authorization_url/client_credentials.rs b/examples/authorization_sign_in/client_credentials.rs similarity index 90% rename from examples/oauth_authorization_url/client_credentials.rs rename to examples/authorization_sign_in/client_credentials.rs index 42377324..4f4dc76b 100644 --- a/examples/oauth_authorization_url/client_credentials.rs +++ b/examples/authorization_sign_in/client_credentials.rs @@ -1,5 +1,5 @@ use graph_oauth::oauth::ClientSecretCredential; -use graph_rs_sdk::{error::IdentityResult, oauth::ClientCredentialsAuthorizationUrlParameters}; +use graph_rs_sdk::{error::IdentityResult, identity::ClientCredentialsAuthorizationUrlParameters}; // The client_id must be changed before running this example. static CLIENT_ID: &str = "<CLIENT_ID>"; diff --git a/examples/oauth_authorization_url/legacy/implicit_grant.rs b/examples/authorization_sign_in/legacy/implicit_grant.rs similarity index 94% rename from examples/oauth_authorization_url/legacy/implicit_grant.rs rename to examples/authorization_sign_in/legacy/implicit_grant.rs index 66ef0e6e..71435338 100644 --- a/examples/oauth_authorization_url/legacy/implicit_grant.rs +++ b/examples/authorization_sign_in/legacy/implicit_grant.rs @@ -22,8 +22,8 @@ use std::collections::BTreeSet; // 2. Implicit grant flow for v2.0: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow // // To better understand OAuth V2.0 and the implicit flow see: https://tools.ietf.org/html/rfc6749#section-1.3.2 -use graph_rs_sdk::oauth::legacy::ImplicitCredential; -use graph_rs_sdk::oauth::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; +use graph_rs_sdk::identity::legacy::ImplicitCredential; +use graph_rs_sdk::identity::{Prompt, ResponseMode, ResponseType, TokenCredentialExecutor}; fn oauth_implicit_flow() -> anyhow::Result<()> { let credential = ImplicitCredential::builder("<YOUR_CLIENT_ID>") diff --git a/examples/oauth_authorization_url/legacy/mod.rs b/examples/authorization_sign_in/legacy/mod.rs similarity index 100% rename from examples/oauth_authorization_url/legacy/mod.rs rename to examples/authorization_sign_in/legacy/mod.rs diff --git a/examples/oauth_authorization_url/main.rs b/examples/authorization_sign_in/main.rs similarity index 98% rename from examples/oauth_authorization_url/main.rs rename to examples/authorization_sign_in/main.rs index ebf4f53e..fdd92afb 100644 --- a/examples/oauth_authorization_url/main.rs +++ b/examples/authorization_sign_in/main.rs @@ -13,7 +13,7 @@ mod client_credentials; mod legacy; mod openid_connect; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, diff --git a/examples/oauth_authorization_url/openid_connect.rs b/examples/authorization_sign_in/openid_connect.rs similarity index 99% rename from examples/oauth_authorization_url/openid_connect.rs rename to examples/authorization_sign_in/openid_connect.rs index a601204d..4635c7e0 100644 --- a/examples/oauth_authorization_url/openid_connect.rs +++ b/examples/authorization_sign_in/openid_connect.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ ConfidentialClientApplication, OpenIdCredential, Prompt, ResponseMode, ResponseType, }; use graph_rs_sdk::GraphClient; diff --git a/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs b/examples/certificate_auth/auth_code_grant/auth_code_certificate.rs similarity index 63% rename from examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs rename to examples/certificate_auth/auth_code_grant/auth_code_certificate.rs index 0a730da9..3fb2e32a 100644 --- a/examples/oauth_certificate/auth_code_grant/auth_code_certificate.rs +++ b/examples/certificate_auth/auth_code_grant/auth_code_certificate.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, X509, }; @@ -6,6 +6,7 @@ use graph_rs_sdk::GraphClient; use std::fs::File; use std::io::Read; use std::path::Path; +use url::Url; pub fn x509_certificate( client_id: &str, @@ -34,35 +35,16 @@ fn build_confidential_client( client_id: &str, tenant: &str, scope: Vec<&str>, - redirect_uri: &str, + redirect_uri: Url, x509certificate: X509Certificate, -) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCertificateCredential>> { - Ok(ConfidentialClientApplication::builder(client_id) +) -> anyhow::Result<GraphClient> { + // ConfidentialClientApplication<AuthorizationCodeCertificateCredential> + let confidential_client = ConfidentialClientApplication::builder(client_id) .with_auth_code_x509_certificate(authorization_code, &x509certificate)? .with_tenant(tenant) .with_scope(scope) - .with_redirect_uri(redirect_uri)? - .build()) -} - -fn build_graph_client( - authorization_code: &str, - client_id: &str, - tenant: &str, - scope: Vec<&str>, - redirect_uri: &str, - x509certificate: X509Certificate, -) -> anyhow::Result<()> { - let confidential_client = build_confidential_client( - authorization_code, - client_id, - tenant, - scope, - redirect_uri, - x509certificate, - )?; - - let _graph_client = GraphClient::from(&confidential_client); + .with_redirect_uri(redirect_uri) + .build(); - Ok(()) + Ok(GraphClient::from(&confidential_client)) } diff --git a/examples/certificate_auth/auth_code_grant/interactive_auth.rs b/examples/certificate_auth/auth_code_grant/interactive_auth.rs new file mode 100644 index 00000000..cd968052 --- /dev/null +++ b/examples/certificate_auth/auth_code_grant/interactive_auth.rs @@ -0,0 +1,51 @@ +use graph_rs_sdk::identity::{ + web::WithInteractiveAuth, AuthorizationCodeCertificateCredential, + ConfidentialClientApplication, MapCredentialBuilder, PKey, X509Certificate, X509, +}; +use graph_rs_sdk::GraphClient; +use std::fs::File; +use std::io::Read; +use std::path::Path; +use url::Url; +pub fn x509_certificate( + client_id: &str, + tenant: &str, + public_key_path: impl AsRef<Path>, + private_key_path: impl AsRef<Path>, +) -> anyhow::Result<X509Certificate> { + // Use include_bytes!(file_path) if the files are local + let mut cert_file = File::open(public_key_path)?; + let mut certificate: Vec<u8> = Vec::new(); + cert_file.read_to_end(&mut certificate)?; + + let mut private_key_file = File::open(private_key_path)?; + let mut private_key: Vec<u8> = Vec::new(); + private_key_file.read_to_end(&mut private_key)?; + + let cert = X509::from_pem(certificate.as_slice())?; + let pkey = PKey::private_key_from_pem(private_key.as_slice())?; + Ok(X509Certificate::new_with_tenant( + client_id, tenant, cert, pkey, + )) +} + +fn interactive_auth( + client_id: &str, + tenant: &str, + scope: Vec<&str>, + redirect_uri: Url, + x509certificate: X509Certificate, +) -> anyhow::Result<GraphClient> { + let (authorization_response, credential_builder) = + ConfidentialClientApplication::builder(client_id) + .auth_code_url_builder() + .with_tenant(tenant) + .with_scope(scope) + .with_redirect_uri(redirect_uri) + .with_interactive_auth(&x509certificate, Default::default()) + .map_to_credential_builder() + .unwrap(); + + let confidential_client = credential_builder.build(); + Ok(GraphClient::from(&confidential_client)) +} diff --git a/examples/oauth_certificate/auth_code_grant/mod.rs b/examples/certificate_auth/auth_code_grant/mod.rs similarity index 68% rename from examples/oauth_certificate/auth_code_grant/mod.rs rename to examples/certificate_auth/auth_code_grant/mod.rs index b7917864..0ef2ca91 100644 --- a/examples/oauth_certificate/auth_code_grant/mod.rs +++ b/examples/certificate_auth/auth_code_grant/mod.rs @@ -1,2 +1,3 @@ mod auth_code_certificate; +mod interactive_auth; mod server_example; diff --git a/examples/oauth_certificate/auth_code_grant/server_example/mod.rs b/examples/certificate_auth/auth_code_grant/server_example/mod.rs similarity index 95% rename from examples/oauth_certificate/auth_code_grant/server_example/mod.rs rename to examples/certificate_auth/auth_code_grant/server_example/mod.rs index 11161101..edae0710 100644 --- a/examples/oauth_certificate/auth_code_grant/server_example/mod.rs +++ b/examples/certificate_auth/auth_code_grant/server_example/mod.rs @@ -1,10 +1,11 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, X509, }; use graph_rs_sdk::GraphClient; use std::fs::File; use std::io::Read; +use url::Url; use warp::Filter; // Requires feature openssl be enabled for graph-rs-sdk or graph-oauth @@ -36,7 +37,7 @@ static CLIENT_ID: &str = "<CLIENT_ID>"; // Only required for certain applications. Used here as an example. static TENANT: &str = "<TENANT_ID>"; -static REDIRECT_URI: &str = "http://localhost:8000/redirect"; +static REDIRECT_URI: Url = Url::parse("http://localhost:8000/redirect").unwrap(); static SCOPE: &str = "User.Read"; @@ -54,7 +55,7 @@ pub struct AccessCode { pub fn authorization_sign_in() { let url = AuthorizationCodeCertificateCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(REDIRECT_URI.clone()) .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -88,7 +89,7 @@ fn build_confidential_client( .with_auth_code_x509_certificate(authorization_code, &x509certificate)? .with_tenant(TENANT) .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI)? + .with_redirect_uri(REDIRECT_URI.clone())? .build()) } diff --git a/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs b/examples/certificate_auth/client_credentials/client_credentials_certificate.rs similarity index 98% rename from examples/oauth_certificate/client_credentials/client_credentials_certificate.rs rename to examples/certificate_auth/client_credentials/client_credentials_certificate.rs index 26fef7f5..91dfa840 100644 --- a/examples/oauth_certificate/client_credentials/client_credentials_certificate.rs +++ b/examples/certificate_auth/client_credentials/client_credentials_certificate.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ ClientCertificateCredential, ConfidentialClientApplication, PKey, X509Certificate, X509, }; use graph_rs_sdk::GraphClient; diff --git a/examples/oauth_certificate/client_credentials/mod.rs b/examples/certificate_auth/client_credentials/mod.rs similarity index 100% rename from examples/oauth_certificate/client_credentials/mod.rs rename to examples/certificate_auth/client_credentials/mod.rs diff --git a/examples/oauth_certificate/main.rs b/examples/certificate_auth/main.rs similarity index 100% rename from examples/oauth_certificate/main.rs rename to examples/certificate_auth/main.rs diff --git a/examples/oauth/README.md b/examples/identity_platform_auth/README.md similarity index 100% rename from examples/oauth/README.md rename to examples/identity_platform_auth/README.md diff --git a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs similarity index 98% rename from examples/oauth/auth_code_grant/auth_code_grant_pkce.rs rename to examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs index b4f114db..abd1e116 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, ProofKeyCodeExchange, TokenCredentialExecutor, }; diff --git a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_secret.rs similarity index 87% rename from examples/oauth/auth_code_grant/auth_code_grant_secret.rs rename to examples/identity_platform_auth/auth_code_grant/auth_code_grant_secret.rs index 743c7dc3..96d74c6a 100644 --- a/examples/oauth/auth_code_grant/auth_code_grant_secret.rs +++ b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_secret.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::error::ErrorMessage; -use graph_rs_sdk::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; +use graph_rs_sdk::identity::{AuthorizationCodeCredential, ConfidentialClientApplication}; use graph_rs_sdk::*; use url::Url; use warp::Filter; @@ -10,7 +10,7 @@ use warp::Filter; /// to in order to sign in. Then wait for the redirect after sign in to the redirect url /// you specified in your app. To see a server example listening for the redirect see /// [Auth Code Grant PKCE Server Example](https://github.com/sreeise/graph-rs-sdk/examples/oauth/auth_code_grant/auth_code_grant_secret.rs) -pub fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> Url { +pub fn authorization_sign_in_url(client_id: &str, redirect_uri: Url, scope: Vec<String>) -> Url { AuthorizationCodeCredential::authorization_url_builder(client_id) .with_redirect_uri(redirect_uri) .with_scope(scope) @@ -23,13 +23,13 @@ async fn auth_code_grant_secret( client_id: &str, client_secret: &str, scope: Vec<String>, - redirect_uri: &str, + redirect_uri: Url, ) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(redirect_uri) .build(); let graph_client = GraphClient::from(&confidential_client); diff --git a/examples/oauth/auth_code_grant/mod.rs b/examples/identity_platform_auth/auth_code_grant/mod.rs similarity index 100% rename from examples/oauth/auth_code_grant/mod.rs rename to examples/identity_platform_auth/auth_code_grant/mod.rs diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs similarity index 99% rename from examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs rename to examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs index ec326d42..bc00f51e 100644 --- a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_pkce.rs +++ b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce, ProofKeyCodeExchange, ResponseType, Token, TokenCredentialExecutor, }; diff --git a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs similarity index 99% rename from examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs rename to examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs index 030c1f73..f38cb1c3 100644 --- a/examples/oauth/auth_code_grant/server_examples/auth_code_grant_secret.rs +++ b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::error::ErrorMessage; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthCodeAuthorizationUrlParameters, AuthorizationCodeCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; diff --git a/examples/oauth/auth_code_grant/server_examples/mod.rs b/examples/identity_platform_auth/auth_code_grant/server_examples/mod.rs similarity index 100% rename from examples/oauth/auth_code_grant/server_examples/mod.rs rename to examples/identity_platform_auth/auth_code_grant/server_examples/mod.rs diff --git a/examples/oauth/client_credentials/client_credentials_secret.rs b/examples/identity_platform_auth/client_credentials/client_credentials_secret.rs similarity index 90% rename from examples/oauth/client_credentials/client_credentials_secret.rs rename to examples/identity_platform_auth/client_credentials/client_credentials_secret.rs index 8fc5092c..56ca474a 100644 --- a/examples/oauth/client_credentials/client_credentials_secret.rs +++ b/examples/identity_platform_auth/client_credentials/client_credentials_secret.rs @@ -3,7 +3,7 @@ // has been granted to your app beforehand. If you have not granted admin consent, see // examples/client_credentials_admin_consent.rs for more info. -use graph_rs_sdk::{oauth::ConfidentialClientApplication, GraphClient}; +use graph_rs_sdk::{identity::ConfidentialClientApplication, GraphClient}; pub async fn build_client(client_id: &str, client_secret: &str, tenant: &str) -> GraphClient { let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) diff --git a/examples/oauth/client_credentials/mod.rs b/examples/identity_platform_auth/client_credentials/mod.rs similarity index 93% rename from examples/oauth/client_credentials/mod.rs rename to examples/identity_platform_auth/client_credentials/mod.rs index 4f3020d9..1e8fd53a 100644 --- a/examples/oauth/client_credentials/mod.rs +++ b/examples/identity_platform_auth/client_credentials/mod.rs @@ -13,4 +13,4 @@ mod client_credentials_secret; mod server_examples; -use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; +use graph_rs_sdk::{identity::ConfidentialClientApplication, Graph}; diff --git a/examples/oauth/client_credentials/server_examples/client_credentials_admin_consent.rs b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs similarity index 98% rename from examples/oauth/client_credentials/server_examples/client_credentials_admin_consent.rs rename to examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs index 79808156..6c229869 100644 --- a/examples/oauth/client_credentials/server_examples/client_credentials_admin_consent.rs +++ b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs @@ -21,7 +21,7 @@ // or admin. See examples/client_credentials.rs use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::oauth::ConfidentialClientApplication; +use graph_rs_sdk::identity::ConfidentialClientApplication; use warp::Filter; // The client_id must be changed before running this example. diff --git a/examples/oauth/client_credentials/server_examples/mod.rs b/examples/identity_platform_auth/client_credentials/server_examples/mod.rs similarity index 100% rename from examples/oauth/client_credentials/server_examples/mod.rs rename to examples/identity_platform_auth/client_credentials/server_examples/mod.rs diff --git a/examples/oauth/device_code.rs b/examples/identity_platform_auth/device_code.rs similarity index 84% rename from examples/oauth/device_code.rs rename to examples/identity_platform_auth/device_code.rs index 25c77ab9..959b7f08 100644 --- a/examples/oauth/device_code.rs +++ b/examples/identity_platform_auth/device_code.rs @@ -1,10 +1,9 @@ -use graph_oauth::oauth::ClientSecretCredential; -use graph_rs_sdk::oauth::{ - DeviceCodeCredential, DeviceCodeCredentialBuilder, PublicClientApplication, Token, - TokenCredentialExecutor, +use graph_rs_sdk::identity::{ + ClientSecretCredential, DeviceCodeCredential, DeviceCodeCredentialBuilder, + PublicClientApplication, Token, TokenCredentialExecutor, }; use graph_rs_sdk::GraphResult; -use graph_rs_sdk::{oauth::ConfidentialClientApplication, Graph}; +use graph_rs_sdk::{identity::ConfidentialClientApplication, Graph}; use std::time::Duration; use warp::hyper::body::HttpBody; diff --git a/examples/oauth/environment_credential.rs b/examples/identity_platform_auth/environment_credential.rs similarity index 95% rename from examples/oauth/environment_credential.rs rename to examples/identity_platform_auth/environment_credential.rs index 2922c102..f7837aa0 100644 --- a/examples/oauth/environment_credential.rs +++ b/examples/identity_platform_auth/environment_credential.rs @@ -1,4 +1,4 @@ -use graph_oauth::oauth::EnvironmentCredential; +use graph_rs_sdk::identity::EnvironmentCredential; use graph_rs_sdk::GraphClient; use std::env::VarError; diff --git a/examples/oauth/getting_tokens_manually.rs b/examples/identity_platform_auth/getting_tokens_manually.rs similarity index 98% rename from examples/oauth/getting_tokens_manually.rs rename to examples/identity_platform_auth/getting_tokens_manually.rs index 9aa0d9f7..e77c2622 100644 --- a/examples/oauth/getting_tokens_manually.rs +++ b/examples/identity_platform_auth/getting_tokens_manually.rs @@ -1,5 +1,5 @@ use graph_core::identity::ClientApplication; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCredential, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; diff --git a/examples/oauth/is_access_token_expired.rs b/examples/identity_platform_auth/is_access_token_expired.rs similarity index 91% rename from examples/oauth/is_access_token_expired.rs rename to examples/identity_platform_auth/is_access_token_expired.rs index ef8bc0cf..0397d65a 100644 --- a/examples/oauth/is_access_token_expired.rs +++ b/examples/identity_platform_auth/is_access_token_expired.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::Token; +use graph_rs_sdk::identity::Token; use std::thread; use std::time::Duration; diff --git a/examples/oauth/main.rs b/examples/identity_platform_auth/main.rs similarity index 98% rename from examples/oauth/main.rs rename to examples/identity_platform_auth/main.rs index 075bb39c..88f9f962 100644 --- a/examples/oauth/main.rs +++ b/examples/identity_platform_auth/main.rs @@ -25,7 +25,7 @@ mod getting_tokens_manually; mod is_access_token_expired; mod openid; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, diff --git a/examples/oauth/openid/mod.rs b/examples/identity_platform_auth/openid/mod.rs similarity index 100% rename from examples/oauth/openid/mod.rs rename to examples/identity_platform_auth/openid/mod.rs diff --git a/examples/oauth/openid/openid.rs b/examples/identity_platform_auth/openid/openid.rs similarity index 89% rename from examples/oauth/openid/openid.rs rename to examples/identity_platform_auth/openid/openid.rs index 2bda3bf8..92e4ad18 100644 --- a/examples/oauth/openid/openid.rs +++ b/examples/identity_platform_auth/openid/openid.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{ConfidentialClientApplication, IdToken}; +use graph_rs_sdk::identity::{ConfidentialClientApplication, IdToken}; use graph_rs_sdk::GraphClient; // OpenIdCredential will automatically include the openid scope diff --git a/examples/oauth/openid/server_examples/mod.rs b/examples/identity_platform_auth/openid/server_examples/mod.rs similarity index 100% rename from examples/oauth/openid/server_examples/mod.rs rename to examples/identity_platform_auth/openid/server_examples/mod.rs diff --git a/examples/oauth/openid/server_examples/openid.rs b/examples/identity_platform_auth/openid/server_examples/openid.rs similarity index 99% rename from examples/oauth/openid/server_examples/openid.rs rename to examples/identity_platform_auth/openid/server_examples/openid.rs index bb4ef6c8..067f897d 100644 --- a/examples/oauth/openid/server_examples/openid.rs +++ b/examples/identity_platform_auth/openid/server_examples/openid.rs @@ -1,4 +1,4 @@ -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ ConfidentialClientApplication, IdToken, OpenIdCredential, Prompt, ResponseMode, ResponseType, Token, TokenCredentialExecutor, }; diff --git a/examples/interactive_auth/INTERACTIVE_AUTH.md b/examples/interactive_auth/INTERACTIVE_AUTH.md index e8b2a0cf..8c9e459c 100644 --- a/examples/interactive_auth/INTERACTIVE_AUTH.md +++ b/examples/interactive_auth/INTERACTIVE_AUTH.md @@ -250,16 +250,18 @@ fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, scope: Ve } ``` -Using `into_result` transforms the `Unauthorized` and `Impeded` variants of `AuthorizationEvent` +Using `map_to_credential_builder` transforms the `Unauthorized` and `Impeded` variants of `AuthorizationEvent` into `WebViewError` which is then returned in the result `Result<(AuthorizationResponse, CredentialBuilder), WebViewError>` ```rust +use graph_rs_sdk::identity::{AuthorizationCodeCredential, Secret, WithInteractiveAuth}; + async fn authenticate(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<()> { let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) .with_redirect_uri(redirect_uri)? - .with_interactive_auth_for_secret(Default::default())? + .with_interactive_auth(Secret("client-secret".to_string()), Default::default()) .into_result()?; Ok(()) diff --git a/examples/interactive_auth/auth_code.rs b/examples/interactive_auth/auth_code.rs index aa1dfc7f..24c43cc7 100644 --- a/examples/interactive_auth/auth_code.rs +++ b/examples/interactive_auth/auth_code.rs @@ -1,4 +1,9 @@ -use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; +use graph_rs_sdk::{ + identity::{ + web::WithInteractiveAuth, AuthorizationCodeCredential, MapCredentialBuilder, Secret, + }, + GraphClient, +}; use url::Url; // Requires feature=interactive_authentication @@ -35,8 +40,8 @@ async fn authenticate( .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(Url::parse(redirect_uri)?) - .with_interactive_auth_for_secret(client_secret, Default::default())? - .into_result()?; + .with_interactive_auth(Secret("secret".to_string()), Default::default()) + .map_to_credential_builder()?; debug!("{authorization_query_response:#?}"); diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index e241aa05..cdf9056e 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -1,5 +1,5 @@ use graph_rs_sdk::{ - oauth::{OpenIdCredential, ResponseMode, ResponseType}, + identity::{OpenIdCredential, ResponseMode, ResponseType}, GraphClient, }; use url::Url; diff --git a/examples/interactive_auth/webview_options.rs b/examples/interactive_auth/webview_options.rs index 1a7ddeee..5655b292 100644 --- a/examples/interactive_auth/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -1,4 +1,7 @@ -use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use graph_rs_sdk::identity::{ + web::Theme, web::WebViewOptions, web::WithInteractiveAuth, AuthorizationCodeCredential, + MapCredentialBuilder, Secret, +}; use graph_rs_sdk::GraphClient; use std::collections::HashSet; use std::ops::Add; @@ -9,43 +12,43 @@ use url::Url; fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" - .with_window_title("Sign In") + .window_title("Sign In") // OS specific theme. Windows only. // See wry crate for more info. - .with_theme(Theme::Dark) + .theme(Theme::Dark) // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the // user to log in and the user has not logged in after 20 minutes you may // want to assume the user is idle in some way and close out of the webview window. - .with_timeout(Instant::now().add(Duration::from_secs(1200))) + .timeout(Instant::now().add(Duration::from_secs(1200))) // The webview can store the cookies that were set after sign in so that on the next // sign in the user is automatically logged in through SSO. Or you can clear the browsing // data, cookies in this case, after sign in when the webview window closes. - .with_clear_browsing_data(false) + .clear_browsing_data_on_close(false) // Provide a list of ports to use for interactive authentication. // This assumes that you have http://localhost or http://localhost:port // for each port registered in your ADF application registration. - .with_ports(HashSet::from([8000])) + .ports(HashSet::from([8000])) } #[cfg(unix)] fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" - .with_window_title("Sign In") + .window_title("Sign In") // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the // user to log in and the user has not logged in after 20 minutes you may // want to assume the user is idle in some way and close out of the webview window. - .with_timeout(Instant::now().add(Duration::from_secs(1200))) + .timeout(Instant::now().add(Duration::from_secs(1200))) // The webview can store the cookies that were set after sign in so that on the next // sign in the user is automatically logged in through SSO. Or you can clear the browsing // data, cookies in this case, after sign in when the webview window closes. - .with_clear_browsing_data(false) + .clear_browsing_data_on_close(false) // Provide a list of ports to use for interactive authentication. // This assumes that you have http://localhost or http://localhost:port // for each port registered in your ADF application registration. - .with_ports(HashSet::from([8000])) + .ports(HashSet::from([8000])) } async fn customize_webview( @@ -60,8 +63,8 @@ async fn customize_webview( .with_tenant(tenant_id) .with_scope(scope) .with_redirect_uri(Url::parse(redirect_uri)?) - .with_interactive_auth_for_secret(client_secret, get_webview_options())? - .into_result()?; + .with_interactive_auth(Secret(client_secret.to_string()), get_webview_options()) + .map_to_credential_builder()?; let confidential_client = credential_builder.build(); diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index 4866e12a..bfd4d989 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -14,6 +14,7 @@ base64 = "0.21.0" dyn-clone = "1.0.14" Inflector = "0.11.4" http = "0.2.11" +jsonwebtoken = "9.1.0" parking_lot = "0.12.1" percent-encoding = "2" reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } diff --git a/graph-core/src/cache/token_cache.rs b/graph-core/src/cache/token_cache.rs index f348750a..5655d74a 100644 --- a/graph-core/src/cache/token_cache.rs +++ b/graph-core/src/cache/token_cache.rs @@ -1,4 +1,4 @@ -use crate::identity::ForceTokenRefresh; +use crate::identity::{DecodedJwt, ForceTokenRefresh}; use async_trait::async_trait; use graph_error::AuthExecutionError; @@ -27,4 +27,8 @@ pub trait TokenCache { async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError>; fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); + + fn decoded_jwt(&self) -> Option<&DecodedJwt> { + None + } } diff --git a/graph-core/src/crypto/mod.rs b/graph-core/src/crypto/mod.rs index 051fa20c..12037e47 100644 --- a/graph-core/src/crypto/mod.rs +++ b/graph-core/src/crypto/mod.rs @@ -1,7 +1,5 @@ -mod jwk; mod pkce; -pub use jwk::*; pub use pkce::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; diff --git a/graph-core/src/identity/client_application.rs b/graph-core/src/identity/client_application.rs index 506174d7..bb740099 100644 --- a/graph-core/src/identity/client_application.rs +++ b/graph-core/src/identity/client_application.rs @@ -1,3 +1,4 @@ +use crate::identity::DecodedJwt; use async_trait::async_trait; use dyn_clone::DynClone; use graph_error::AuthExecutionResult; @@ -26,6 +27,10 @@ pub trait ClientApplication: DynClone + Send + Sync { async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); + + fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { + None + } } #[async_trait] diff --git a/graph-core/src/crypto/jwk.rs b/graph-core/src/identity/jwk.rs similarity index 100% rename from graph-core/src/crypto/jwk.rs rename to graph-core/src/identity/jwk.rs diff --git a/graph-core/src/identity/jwks.rs b/graph-core/src/identity/jwks.rs new file mode 100644 index 00000000..68224526 --- /dev/null +++ b/graph-core/src/identity/jwks.rs @@ -0,0 +1,72 @@ +use jsonwebtoken::TokenData; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct JwksKeySet { + pub keys: HashSet<JwksKey>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] +pub struct JwksKey { + pub kid: String, + #[serde(alias = "n")] + pub modulus: String, + #[serde(alias = "e")] + pub exponent: String, +} + +impl JwksKey { + pub fn new(kid: impl ToString, modulus: impl ToString, exponent: impl ToString) -> JwksKey { + JwksKey { + kid: kid.to_string(), + modulus: modulus.to_string(), + exponent: exponent.to_string(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct JwtHeader { + pub typ: String, + pub alg: String, + pub kid: String, + pub x5t: Option<String>, +} + +impl Display for JwtHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "typ: {}, alg: {}, kid: {}, x5t: {:#?}", + self.typ, self.alg, self.kid, self.x5t + ) + } +} + +pub type DecodedJwt = TokenData<Claims>; + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct Claims { + pub aud: String, + pub iss: String, + pub iat: usize, + pub nbf: usize, + pub exp: usize, + pub aio: Option<String>, + pub c_hash: Option<String>, + pub cc: Option<String>, + pub email: Option<String>, + pub name: Option<String>, + pub nonce: Option<String>, + pub oid: Option<String>, + pub preferred_username: Option<String>, + pub rh: Option<String>, + pub sub: Option<String>, + pub tid: Option<String>, + pub uti: Option<String>, + pub ver: Option<String>, + #[serde(flatten)] + pub additional_fields: HashMap<String, Value>, +} diff --git a/graph-core/src/identity/mod.rs b/graph-core/src/identity/mod.rs index 6975f88e..5b3839f0 100644 --- a/graph-core/src/identity/mod.rs +++ b/graph-core/src/identity/mod.rs @@ -1,3 +1,7 @@ mod client_application; +mod jwk; +mod jwks; pub use client_application::*; +pub use jwk::*; +pub use jwks::*; diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 4b0d0603..1070d5b6 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -17,6 +17,7 @@ futures = "0.3" handlebars = "2.0.2" http-serde = "1" http = "0.2.11" +jsonwebtoken = "9.1.0" reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.16.15" serde = { version = "1", features = ["derive"] } diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 5953a8a3..94f92c15 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -105,6 +105,9 @@ pub enum AuthExecutionError { message: String, response: http::Response<Result<serde_json::Value, ErrorMessage>>, }, + + #[error("{0:#?}")] + JsonWebToken(#[from] jsonwebtoken::errors::Error), } impl AuthExecutionError { diff --git a/graph-error/src/graph_failure.rs b/graph-error/src/graph_failure.rs index 48e58a84..314762b9 100644 --- a/graph-error/src/graph_failure.rs +++ b/graph-error/src/graph_failure.rs @@ -79,6 +79,9 @@ pub enum GraphFailure { message: String, response: http::Response<Result<serde_json::Value, ErrorMessage>>, }, + + #[error("{0:#?}")] + JsonWebToken(#[from] jsonwebtoken::errors::Error), } impl GraphFailure { @@ -153,6 +156,7 @@ impl From<AuthExecutionError> for GraphFailure { AuthExecutionError::SilentTokenAuth { message, response } => { GraphFailure::SilentTokenAuth { message, response } } + AuthExecutionError::JsonWebToken(error) => GraphFailure::JsonWebToken(error), } } } diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index d500d569..b233fc38 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,5 +1,5 @@ use crate::blocking::BlockingClient; -use graph_core::identity::{ClientApplication, ForceTokenRefresh}; +use graph_core::identity::{ClientApplication, DecodedJwt, ForceTokenRefresh}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -263,6 +263,10 @@ impl Client { self.client_application .with_force_token_refresh(force_token_refresh); } + + pub fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { + self.client_application.get_decoded_jwt() + } } impl Default for Client { diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 088e2088..bfa3852c 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -39,6 +39,7 @@ wry = { version = "0.33.1", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" +tracing-futures = "0.2.5" graph-error = { path = "../graph-error" } graph-core = { path = "../graph-core", default-features = false } diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index 4f0aaf80..ee8e7c81 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -2,7 +2,7 @@ use url::Url; use uuid::Uuid; use crate::identity::AadAuthorityAudience; -use crate::oauth::AzureCloudInstance; +use crate::AzureCloudInstance; /// Application Options typically stored as JSON file in .net applications. #[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] @@ -58,9 +58,10 @@ mod test { #[test] fn application_options_from_file() { - let file = - File::open(r#"./src/identity/credentials/test/application_options/aad_options.json"#) - .unwrap(); + let file = File::open( + r#"../../../src/identity_/credentials/test/application_options/aad_options.json"#, + ) + .unwrap(); let application_options: ApplicationOptions = serde_json::from_reader(file).unwrap(); assert_eq!( application_options.aad_authority_audience, diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index 1abc973d..b8469419 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,4 +1,3 @@ -use crate::identity::AppConfig; use graph_error::{WebViewError, WebViewResult}; use serde::Deserializer; use serde_json::Value; @@ -12,7 +11,7 @@ use url::Url; /// Microsoft has additional errors listed here: /// https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#error-codes-for-authorization-endpoint-errors #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum AuthorizationQueryError { +pub enum AuthorizationResponseError { /// The request is missing a required parameter, includes an /// invalid parameter value, includes a parameter more than /// once, or is otherwise malformed. @@ -80,7 +79,7 @@ pub enum AuthorizationQueryError { InteractionRequired, } -impl Display for AuthorizationQueryError { +impl Display for AuthorizationResponseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{self:#?}") } @@ -112,7 +111,7 @@ pub(crate) struct PhantomAuthorizationResponse { pub state: Option<String>, pub session_state: Option<String>, pub nonce: Option<String>, - pub error: Option<AuthorizationQueryError>, + pub error: Option<AuthorizationResponseError>, pub error_description: Option<String>, pub error_uri: Option<Url>, #[serde(flatten)] @@ -123,7 +122,7 @@ pub(crate) struct PhantomAuthorizationResponse { #[derive(Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct AuthorizationError { - pub error: Option<AuthorizationQueryError>, + pub error: Option<AuthorizationResponseError>, pub error_description: Option<String>, pub error_uri: Option<Url>, } @@ -139,25 +138,20 @@ pub struct AuthorizationResponse { pub state: Option<String>, pub session_state: Option<String>, pub nonce: Option<String>, - pub error: Option<AuthorizationQueryError>, + pub error: Option<AuthorizationResponseError>, pub error_description: Option<String>, pub error_uri: Option<Url>, #[serde(flatten)] pub additional_fields: HashMap<String, Value>, + /// When true debug logging will log personally identifiable information such + /// as the id_token. This is disabled by default. When log_pii is enabled + /// passing [AuthorizationResponse] to logging or print functions will log the access token + /// and id token value. #[serde(skip)] - log_pii: bool, + pub log_pii: bool, } impl AuthorizationResponse { - /// Enable or disable logging of personally identifiable information such - /// as logging the id_token. This is disabled by default. When log_pii is enabled - /// passing [AuthorizationResponse] to logging or print functions will log both the bearer - /// access token value of amy and the id token value. - /// By default these do not get logged. - pub fn enable_pii_logging(&mut self, log_pii: bool) { - self.log_pii = log_pii; - } - pub fn is_err(&self) -> bool { self.error.is_some() } @@ -231,14 +225,17 @@ impl<CredentialBuilder: Clone + Debug> AuthorizationEvent<CredentialBuilder> { } } -pub trait IntoCredentialBuilder<CredentialBuilder: Clone + Debug> { - fn into_credential_builder(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)>; +pub trait MapCredentialBuilder<CredentialBuilder: Clone + Debug> { + fn map_to_credential_builder(self) + -> WebViewResult<(AuthorizationResponse, CredentialBuilder)>; } -impl<CredentialBuilder: Clone + Debug> IntoCredentialBuilder<CredentialBuilder> +impl<CredentialBuilder: Clone + Debug> MapCredentialBuilder<CredentialBuilder> for WebViewResult<AuthorizationEvent<CredentialBuilder>> { - fn into_credential_builder(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { + fn map_to_credential_builder( + self, + ) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { match self { Ok(auth_event) => match auth_event { AuthorizationEvent::Authorized { diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 08535559..160f17fc 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -9,8 +9,8 @@ use reqwest::header::HeaderMap; use url::Url; use uuid::Uuid; -use crate::identity::{Authority, AzureCloudInstance}; -use crate::oauth::ApplicationOptions; +use crate::identity::{Authority, AzureCloudInstance, IdToken}; +use crate::ApplicationOptions; #[derive(Clone, Default, PartialEq)] pub struct AppConfig { @@ -64,6 +64,7 @@ pub struct AppConfig { /// Cache id used in a token cache store. pub(crate) cache_id: String, pub(crate) force_token_refresh: ForceTokenRefresh, + pub(crate) id_token: Option<IdToken>, pub(crate) log_pii: bool, } @@ -91,6 +92,7 @@ impl TryFrom<ApplicationOptions> for AppConfig { ), cache_id, force_token_refresh: Default::default(), + id_token: Default::default(), log_pii: false, }) } @@ -169,6 +171,7 @@ impl AppConfig { ), cache_id, force_token_refresh: Default::default(), + id_token: Default::default(), log_pii: Default::default(), } } @@ -234,6 +237,10 @@ impl AppConfig { pub(crate) fn with_scope<T: ToString, I: IntoIterator<Item = T>>(&mut self, scope: I) { self.scope = scope.into_iter().map(|s| s.to_string()).collect(); } + + pub(crate) fn with_id_token(&mut self, id_token: IdToken) { + self.id_token = Some(id_token); + } } #[derive(Clone, Default, PartialEq)] diff --git a/graph-oauth/src/identity/credentials/application_builder.rs b/graph-oauth/src/identity/credentials/application_builder.rs index a08b6853..c56747ab 100644 --- a/graph-oauth/src/identity/credentials/application_builder.rs +++ b/graph-oauth/src/identity/credentials/application_builder.rs @@ -169,7 +169,7 @@ impl ConfidentialClientApplicationBuilder { authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, ) -> AuthorizationCodeAssertionCredentialBuilder { - AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code_and_assertion( + AuthorizationCodeAssertionCredentialBuilder::from_assertion( authorization_code, assertion, self.app_config.clone(), diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index e846095c..83ed3f63 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -8,36 +8,33 @@ use url::Url; use uuid::Uuid; use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; -use graph_error::{AuthorizationFailure, IdentityResult, AF}; +use graph_error::{IdentityResult, AF}; use crate::identity::{ - credentials::app_config::AppConfig, tracing_targets::INTERACTIVE_AUTH, AsQuery, + tracing_targets::INTERACTIVE_AUTH, AppConfig, AsQuery, AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::{Assertion, AuthorizationEvent, Secret}; #[cfg(feature = "openssl")] -use crate::identity::{AuthorizationCodeCertificateCredentialBuilder, X509Certificate}; +use crate::identity::X509Certificate; #[cfg(feature = "interactive-auth")] -use graph_error::{AuthExecutionError, WebViewError, WebViewResult}; - -#[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationResponse, Token}; - -#[cfg(feature = "interactive-auth")] -use crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, -}; - -use crate::oauth::AuthorizationEvent; -#[cfg(feature = "interactive-auth")] -use crate::web::UserEvents; -#[cfg(feature = "interactive-auth")] -use wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, +use { + crate::identity::{ + AuthorizationCodeCertificateCredentialBuilder, AuthorizationResponse, Token, + }, + crate::web::{ + HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, + WebViewOptions, WithInteractiveAuth, + }, + graph_error::{AuthExecutionError, WebViewError, WebViewResult}, + wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, + }, }; credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); @@ -670,12 +667,61 @@ impl AuthCodeAuthorizationUrlParameterBuilder { self } - #[cfg(feature = "interactive-auth")] - pub fn with_interactive_auth_for_secret( + pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { + self.credential.clone() + } + + pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { + self.credential.url_with_host(azure_cloud_instance) + } + + pub fn url(&self) -> IdentityResult<Url> { + self.credential.url() + } + + pub fn with_auth_code( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeCredentialBuilder { + AuthorizationCodeCredentialBuilder::new_with_auth_code( + authorization_code, + self.credential.app_config, + ) + } + + pub fn with_auth_code_assertion( + self, + authorization_code: impl AsRef<str>, + ) -> AuthorizationCodeAssertionCredentialBuilder { + AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( + self.credential.app_config, + authorization_code, + ) + } + + #[cfg(feature = "openssl")] + pub fn with_auth_code_x509_certificate( + self, + authorization_code: impl AsRef<str>, + x509: &X509Certificate, + ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { + AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( + authorization_code, + x509, + self.credential.app_config, + ) + } +} + +#[cfg(feature = "interactive-auth")] +impl WithInteractiveAuth<Secret> for AuthCodeAuthorizationUrlParameterBuilder { + type CredentialBuilder = AuthorizationCodeCredentialBuilder; + + fn with_interactive_auth( &self, - client_secret: impl AsRef<str>, + auth_type: Secret, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeCredentialBuilder>> { + ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; @@ -696,24 +742,28 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } else { AuthorizationCodeCredentialBuilder::new_with_token( self.credential.app_config.clone(), - Token::from(authorization_response.clone()), + Token::try_from(authorization_response.clone())?, ) } }; - credential_builder.with_client_secret(client_secret); + credential_builder.with_client_secret(auth_type.into_inner()); Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, }) } +} - #[cfg(feature = "interactive-auth")] - pub fn with_interactive_auth_for_assertion( +#[cfg(feature = "interactive-auth")] +impl WithInteractiveAuth<Assertion> for AuthCodeAuthorizationUrlParameterBuilder { + type CredentialBuilder = AuthorizationCodeAssertionCredentialBuilder; + + fn with_interactive_auth( &self, - client_assertion: impl AsRef<str>, + auth_type: Assertion, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeAssertionCredentialBuilder>> { + ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; @@ -733,25 +783,29 @@ impl AuthCodeAuthorizationUrlParameterBuilder { } else { AuthorizationCodeAssertionCredentialBuilder::new_with_token( self.credential.app_config.clone(), - Token::from(authorization_response.clone()), + Token::try_from(authorization_response.clone())?, ) } }; - credential_builder.with_client_assertion(client_assertion); + credential_builder.with_client_assertion(auth_type.into_inner()); Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, }) } +} - #[cfg(feature = "interactive-auth")] - #[cfg(feature = "openssl")] - pub fn with_interactive_auth_for_x509_certificate( +#[cfg(feature = "openssl")] +#[cfg(feature = "interactive-auth")] +impl WithInteractiveAuth<&X509Certificate> for AuthCodeAuthorizationUrlParameterBuilder { + type CredentialBuilder = AuthorizationCodeCertificateCredentialBuilder; + + fn with_interactive_auth( &self, - x509: &X509Certificate, + auth_type: &X509Certificate, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<AuthorizationCodeCertificateCredentialBuilder>> { + ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; @@ -766,69 +820,24 @@ impl AuthCodeAuthorizationUrlParameterBuilder { if let Some(authorization_code) = authorization_response.code.as_ref() { AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( authorization_code, - x509, + auth_type, self.credential.app_config.clone(), )? } else { AuthorizationCodeCertificateCredentialBuilder::new_with_token( - Token::from(authorization_response.clone()), - x509, + Token::try_from(authorization_response.clone())?, + auth_type, self.credential.app_config.clone(), )? } }; - credential_builder.with_x509(x509)?; + credential_builder.with_x509(auth_type)?; Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, }) } - - pub fn build(&self) -> AuthCodeAuthorizationUrlParameters { - self.credential.clone() - } - - pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { - self.credential.url_with_host(azure_cloud_instance) - } - - pub fn url(&self) -> IdentityResult<Url> { - self.credential.url() - } - - pub fn with_auth_code( - self, - authorization_code: impl AsRef<str>, - ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new_with_auth_code( - authorization_code, - self.credential.app_config, - ) - } - - pub fn with_auth_code_assertion( - self, - authorization_code: impl AsRef<str>, - ) -> AuthorizationCodeAssertionCredentialBuilder { - AuthorizationCodeAssertionCredentialBuilder::new_with_auth_code( - self.credential.app_config, - authorization_code, - ) - } - - #[cfg(feature = "openssl")] - pub fn with_auth_code_x509_certificate( - self, - authorization_code: impl AsRef<str>, - x509: &X509Certificate, - ) -> IdentityResult<AuthorizationCodeCertificateCredentialBuilder> { - AuthorizationCodeCertificateCredentialBuilder::new_with_auth_code_and_x509( - authorization_code, - x509, - self.credential.app_config, - ) - } } #[cfg(test)] @@ -838,7 +847,7 @@ mod test { #[test] fn serialize_uri() { let authorizer = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .build(); @@ -849,7 +858,7 @@ mod test { #[test] fn url_with_host() { let url_result = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .url_with_host(&AzureCloudInstance::AzureGermany); @@ -860,7 +869,7 @@ mod test { #[should_panic] fn response_type_id_token_panics_when_response_mode_query() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .with_response_mode(ResponseMode::Query) .with_response_type(vec![ResponseType::IdToken]) @@ -873,7 +882,7 @@ mod test { #[test] fn response_mode_not_set() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .url() .unwrap(); @@ -886,7 +895,7 @@ mod test { #[test] fn multi_response_type_set() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .with_response_mode(ResponseMode::FormPost) .with_response_type(vec![ResponseType::IdToken, ResponseType::Code]) @@ -901,7 +910,7 @@ mod test { #[test] fn generate_nonce() { let url = AuthCodeAuthorizationUrlParameters::builder(Uuid::new_v4()) - .with_redirect_uri("https://localhost:8080") + .with_redirect_uri(Url::parse("https://localhost:8080").unwrap()) .with_scope(["read", "write"]) .with_generated_nonce() .url() diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index e7270823..84052b9a 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -394,7 +394,7 @@ impl AuthorizationCodeAssertionCredentialBuilder { } } - pub(crate) fn new_with_auth_code_and_assertion( + pub(crate) fn from_assertion( authorization_code: impl AsRef<str>, assertion: impl AsRef<str>, app_config: AppConfig, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 07861f2c..4c0dc094 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -457,7 +457,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { ) } else { AuthorizationCodeCertificateCredentialBuilder::new_with_token( - Token::from(authorization_response.clone()), + Token::try_from(authorization_response.clone())?, x509, app_config, ) diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 6a4c268a..2c7908ae 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -18,8 +18,8 @@ use crate::identity::{ tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth::AuthCodeAuthorizationUrlParameterBuilder; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::{AuthCodeAuthorizationUrlParameterBuilder, Secret}; credential_builder!( AuthorizationCodeCredentialBuilder, @@ -114,12 +114,12 @@ impl AuthorizationCodeCredential { self.refresh_token = Some(refresh_token.as_ref().to_owned()); } - pub fn builder<T: AsRef<str>, U: AsRef<str>>( - client_id: T, - client_secret: T, - authorization_code: U, + pub fn builder( + authorization_code: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { - AuthorizationCodeCredentialBuilder::new(client_id, client_secret, authorization_code) + AuthorizationCodeCredentialBuilder::new(authorization_code, client_id, client_secret) } pub fn authorization_url_builder( @@ -271,10 +271,10 @@ pub struct AuthorizationCodeCredentialBuilder { } impl AuthorizationCodeCredentialBuilder { - fn new<T: AsRef<str>, U: AsRef<str>>( - client_id: T, - client_secret: T, - authorization_code: U, + fn new( + authorization_code: impl AsRef<str>, + client_id: impl AsRef<str>, + client_secret: impl AsRef<str>, ) -> AuthorizationCodeCredentialBuilder { Self { credential: AuthorizationCodeCredential { @@ -324,6 +324,23 @@ impl AuthorizationCodeCredentialBuilder { } } + pub(crate) fn from_secret( + authorization_code: String, + secret: String, + app_config: AppConfig, + ) -> AuthorizationCodeCredentialBuilder { + Self { + credential: AuthorizationCodeCredential { + app_config, + authorization_code: Some(authorization_code), + refresh_token: None, + client_secret: secret, + code_verifier: None, + token_cache: Default::default(), + }, + } + } + pub fn with_authorization_code<T: AsRef<str>>(&mut self, authorization_code: T) -> &mut Self { self.credential.authorization_code = Some(authorization_code.as_ref().to_owned()); self.credential.refresh_token = None; @@ -503,7 +520,7 @@ impl From<(AppConfig, AuthorizationResponse)> for AuthorizationCodeCredentialBui } else { AuthorizationCodeCredentialBuilder::new_with_token( app_config, - Token::from(authorization_response.clone()), + Token::try_from(authorization_response.clone()).unwrap_or_default(), ) } } @@ -560,8 +577,7 @@ mod test { let mut credential_builder = AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); let mut credential = credential_builder - .with_redirect_uri("https://localhost") - .unwrap() + .with_redirect_uri(Url::parse("http://localhost").unwrap()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id") @@ -577,8 +593,7 @@ mod test { let mut credential_builder = AuthorizationCodeCredential::builder(uuid_value, "secret".to_string(), "code"); let _credential = credential_builder - .with_redirect_uri("https://localhost") - .unwrap() + .with_redirect_uri(Url::parse("http://localhost").unwrap()) .with_client_secret("client_secret") .with_scope(vec!["scope"]) .with_tenant("tenant_id") diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 011e8461..159e8256 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -53,6 +53,14 @@ pub struct ClientAssertionCredential { token_cache: InMemoryCacheStore<Token>, } +impl Debug for ClientAssertionCredential { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ClientAssertionCredential") + .field("app_config", &self.app_config) + .finish() + } +} + impl ClientAssertionCredential { pub fn new( tenant_id: impl AsRef<str>, @@ -102,14 +110,6 @@ impl ClientAssertionCredential { } } -impl Debug for ClientAssertionCredential { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ClientAssertionCredential") - .field("app_config", &self.app_config) - .finish() - } -} - #[async_trait] impl TokenCache for ClientAssertionCredential { type Token = Token; @@ -205,7 +205,7 @@ impl TokenCredentialExecutor for ClientAssertionCredential { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ClientAssertionCredentialBuilder { credential: ClientAssertionCredential, } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index a2f18564..a59dd857 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -6,8 +6,8 @@ use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult}; use crate::identity::{credentials::app_config::AppConfig, Authority, AzureCloudInstance}; -use crate::oauth::{ClientAssertionCredentialBuilder, ClientSecretCredentialBuilder}; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::{ClientAssertionCredentialBuilder, ClientSecretCredentialBuilder}; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 5821e3e1..8fc0a9d0 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -213,7 +213,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ClientSecretCredentialBuilder { credential: ClientSecretCredential, } diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index c5c9ffb1..350c7536 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -2,22 +2,24 @@ use std::collections::HashMap; use std::fmt::Debug; use async_trait::async_trait; +use jsonwebtoken::TokenData; use reqwest::Response; use url::Url; use uuid::Uuid; -use graph_core::cache::{AsBearer, TokenCache}; -use graph_core::identity::{ClientApplication, ForceTokenRefresh}; +use graph_core::identity::{ClientApplication, DecodedJwt, ForceTokenRefresh}; +use graph_core::{ + cache::{AsBearer, TokenCache}, + identity::Claims, +}; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::{ - credentials::app_config::AppConfig, - credentials::application_builder::ConfidentialClientApplicationBuilder, - credentials::client_assertion_credential::ClientAssertionCredential, Authority, - AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, - AuthorizationCodeCredential, AzureCloudInstance, ClientCertificateCredential, - ClientSecretCredential, OpenIdCredential, TokenCredentialExecutor, + AppConfig, Authority, AuthorizationCodeAssertionCredential, + AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, AzureCloudInstance, + ClientAssertionCredential, ClientCertificateCredential, ClientSecretCredential, + ConfidentialClientApplicationBuilder, OpenIdCredential, TokenCredentialExecutor, }; /// Clients capable of maintaining the confidentiality of their credentials @@ -60,8 +62,8 @@ impl<Credential: Clone + Debug + Send + Sync + TokenCredentialExecutor> } #[async_trait] -impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication - for ConfidentialClientApplication<Credential> +impl<Credential: Clone + Debug + Send + Sync + TokenCache + TokenCredentialExecutor> + ClientApplication for ConfidentialClientApplication<Credential> { fn get_token_silent(&mut self) -> AuthExecutionResult<String> { let token = self.credential.get_token_silent()?; @@ -77,6 +79,10 @@ impl<Credential: Clone + Debug + Send + Sync + TokenCache> ClientApplication self.credential .with_force_token_refresh(force_token_refresh); } + + fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { + self.credential.decoded_jwt() + } } #[async_trait] @@ -170,6 +176,12 @@ impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> } } +impl ConfidentialClientApplication<OpenIdCredential> { + pub fn decoded_id_token(&self) -> Option<&TokenData<Claims>> { + self.credential.get_decoded_jwt() + } +} + #[cfg(test)] mod test { use crate::identity::Authority; @@ -185,8 +197,7 @@ mod test { .with_auth_code("code") .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_scope(vec!["Read.Write"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() + .with_redirect_uri(Url::parse("http://localhost:8888/redirect").unwrap()) .build(); let credential_uri = confidential_client.credential.uri().unwrap(); @@ -207,8 +218,7 @@ mod test { .with_tenant("tenant") .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") .with_scope(vec!["Read.Write"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() + .with_redirect_uri(Url::parse("http://localhost:8888/redirect").unwrap()) .build(); let credential_uri = confidential_client.credential.uri().unwrap(); @@ -228,9 +238,8 @@ mod test { .with_auth_code("code") .with_authority(Authority::Consumers) .with_client_secret("ALDSKFJLKERLKJALSDKJF2209LAKJGFL") - .with_scope(vec!["Read.Write", "Fall.Down"]) - .with_redirect_uri("http://localhost:8888/redirect") - .unwrap() + .with_scope(vec!["Read.Write"]) + .with_redirect_uri(Url::parse("http://localhost:8888/redirect").unwrap()) .build(); let credential_uri = confidential_client.credential.uri().unwrap(); diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index ee10f15c..8cb6f40b 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -63,3 +63,19 @@ pub(crate) mod tracing_targets { pub const CREDENTIAL_EXECUTOR: &str = "graph_rs_sdk::credential_executor"; pub const INTERACTIVE_AUTH: &str = "graph_rs_sdk::interactive_auth"; } + +pub struct Secret(pub String); + +impl Secret { + pub(crate) fn into_inner(self) -> String { + self.0 + } +} + +pub struct Assertion(pub String); + +impl Assertion { + pub(crate) fn into_inner(self) -> String { + self.0 + } +} diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index f67a760a..e79ebfe4 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -27,9 +27,10 @@ use crate::web::{ HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, }; -use crate::oauth::{AuthorizationEvent, PhantomAuthorizationResponse}; +use crate::identity::tracing_targets::CREDENTIAL_EXECUTOR; #[cfg(feature = "interactive-auth")] use crate::web::UserEvents; +use crate::{AuthorizationEvent, PhantomAuthorizationResponse}; #[cfg(feature = "interactive-auth")] use wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -122,6 +123,7 @@ pub struct OpenIdAuthorizationUrlParameters { /// this parameter during re-authentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, + verify_id_token: bool, } impl Debug for OpenIdAuthorizationUrlParameters { @@ -159,6 +161,7 @@ impl OpenIdAuthorizationUrlParameters { prompt: Default::default(), domain_hint: None, login_hint: None, + verify_id_token: Default::default(), }) } @@ -172,6 +175,7 @@ impl OpenIdAuthorizationUrlParameters { prompt: Default::default(), domain_hint: None, login_hint: None, + verify_id_token: Default::default(), } } @@ -180,7 +184,11 @@ impl OpenIdAuthorizationUrlParameters { } pub fn into_credential(self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { - OpenIdCredentialBuilder::new_with_auth_code(self.app_config, authorization_code) + OpenIdCredentialBuilder::new_with_auth_code( + self.app_config, + authorization_code, + self.verify_id_token, + ) } pub fn url(&self) -> IdentityResult<Url> { @@ -267,8 +275,13 @@ impl OpenIdAuthorizationUrlParameters { self.app_config.clone(), authorization_response.clone(), )); + credential_builder.with_client_secret(client_secret); + if self.verify_id_token { + credential_builder.with_id_token_verification(true); + } + Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, @@ -318,19 +331,20 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { serializer.response_type(ResponseType::Code); } else { let response_types = self.response_type.as_query(); - dbg!(response_types.as_str()); if !RESPONSE_TYPES_SUPPORTED.contains(&response_types.as_str()) { - return AuthorizationFailure::msg_result( - "response_type", - format!( - "provided response_type is not supported - supported response types are: {}", - RESPONSE_TYPES_SUPPORTED - .iter() - .map(|s| format!("`{}`", s)) - .collect::<Vec<String>>() - .join(", ") - ), + let err = format!( + "provided response_type is not supported - supported response types are: {}", + RESPONSE_TYPES_SUPPORTED + .iter() + .map(|s| format!("`{}`", s)) + .collect::<Vec<String>>() + .join(", ") + ); + tracing::error!( + target: CREDENTIAL_EXECUTOR, + err ); + return AuthorizationFailure::msg_result("response_type", err); } serializer.response_types(self.response_type.iter()); @@ -561,6 +575,11 @@ impl OpenIdAuthorizationUrlParameterBuilder { self } + pub fn with_id_token_verification(&mut self, verify_id_token: bool) -> &mut Self { + self.credential.verify_id_token = verify_id_token; + self + } + #[cfg(feature = "interactive-auth")] pub fn with_interactive_auth( &self, @@ -587,6 +606,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { OpenIdCredentialBuilder::new_with_auth_code( self.credential.app_config.clone(), authorization_code, + false, ) } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 0c05dae9..ce32bf50 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -4,25 +4,29 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; +use jsonwebtoken::errors::ErrorKind; +use jsonwebtoken::TokenData; use reqwest::IntoUrl; use url::{ParseError, Url}; use uuid::Uuid; -use graph_core::crypto::{GenPkce, ProofKeyCodeExchange}; -use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; -use graph_core::identity::ForceTokenRefresh; +use graph_core::{ + crypto::{GenPkce, ProofKeyCodeExchange}, + http::{AsyncResponseConverterExt, ResponseConverterExt}, + identity::{Claims, DecodedJwt, ForceTokenRefresh, JwksKeySet}, +}; + use graph_error::{ AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult, AF, }; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, - OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, - TokenCredentialExecutor, + tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance, + ConfidentialClientApplication, IdToken, OpenIdAuthorizationUrlParameterBuilder, + OpenIdAuthorizationUrlParameters, Token, TokenCredentialExecutor, }; use crate::internal::{OAuthParameter, OAuthSerializer}; -use crate::oauth::JwtHeader; credential_builder!( OpenIdCredentialBuilder, @@ -65,6 +69,8 @@ pub struct OpenIdCredential { serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, openid_config: Option<serde_json::Value>, + verify_id_token: bool, + id_token_jwt: Option<DecodedJwt>, } impl Debug for OpenIdCredential { @@ -96,6 +102,8 @@ impl OpenIdCredential { serializer: Default::default(), token_cache: Default::default(), openid_config: None, + verify_id_token: Default::default(), + id_token_jwt: None, }) } @@ -120,6 +128,10 @@ impl OpenIdCredential { self.pkce.as_ref() } + pub(crate) fn get_decoded_jwt(&self) -> Option<&TokenData<Claims>> { + self.id_token_jwt.as_ref() + } + pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { let uri = self .app_config @@ -164,38 +176,141 @@ impl OpenIdCredential { .map_err(AuthExecutionError::from) } - pub fn get_jwks_key(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + pub fn verify_jwks(&self) -> AuthExecutionResult<TokenData<Claims>> { + let cache_id = self.app_config.cache_id.to_string(); + let token = self + .token_cache + .get(cache_id.as_str()) + .ok_or(AF::msg_err("token", "no cached token"))?; + let mut id_token = token + .id_token + .clone() + .ok_or(AF::msg_err("id_token", "no cached id_token"))?; + self.verify_jwks_from_token(&mut id_token) + } + + pub async fn verify_jwks_async(&self) -> AuthExecutionResult<TokenData<Claims>> { + let cache_id = self.app_config.cache_id.to_string(); + let token = self + .token_cache + .get(cache_id.as_str()) + .ok_or(AF::msg_err("token", "no cached token"))?; + let mut id_token = token + .id_token + .clone() + .ok_or(AF::msg_err("id_token", "no cached id_token"))?; + self.verify_jwks_from_token_async(&mut id_token).await + } + + fn verify_jwks_from_token( + &self, + id_token: &mut IdToken, + ) -> AuthExecutionResult<TokenData<Claims>> { + let headers = id_token.decode_header()?; + let kid = headers + .kid + .as_ref() + .ok_or(AF::msg_err("id_token", "id_token header does not have kid"))?; + let response = self.get_jwks()?; - let json: serde_json::Value = response.json()?; - let keys = json["keys"].as_array().ok_or(AF::msg_err( - "keys", - "required but not found in json web key set", - ))?; - keys.iter() - .find(|value| value["kid"].as_str().eq(&Some(kid))) + let status = response.status(); + + tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks key set response received; status={status:#?}"); + + let key_set: JwksKeySet = response.json()?; + let jwks_key = key_set + .keys + .iter() + .find(|key| key.kid.eq(kid)) .cloned() - .ok_or(AF::msg_err("kid", "no match found in json web keys")) - .map_err(AuthExecutionError::from) + .ok_or(AF::msg_err( + "kid", + "no match found for kid in json web keys", + )) + .map_err(AuthExecutionError::from)?; + + tracing::debug!(target: CREDENTIAL_EXECUTOR, "found matching kid in jwks key set"); + + if self.app_config.tenant_id.is_some() { + Ok(id_token.decode( + jwks_key.modulus.as_str(), + jwks_key.exponent.as_str(), + &self.app_config.client_id.to_string(), + Some(self.issuer().map_err(AuthorizationFailure::from)?.as_str()), + )?) + } else { + Ok(id_token.decode( + jwks_key.modulus.as_str(), + jwks_key.exponent.as_str(), + &self.app_config.client_id.to_string(), + None, + )?) + } } - pub async fn get_jwks_key_async(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { + async fn verify_jwks_from_token_async( + &self, + id_token: &mut IdToken, + ) -> AuthExecutionResult<TokenData<Claims>> { + let headers = id_token.decode_header()?; + let value2 = serde_json::to_string(&headers).unwrap(); + tracing::debug!( + target: CREDENTIAL_EXECUTOR, + value2 + ); + + let kid = headers + .kid + .as_ref() + .ok_or(AF::msg_err("id_token", "id_token header does not have kid"))?; + let response = self.get_jwks_async().await?; - let json: serde_json::Value = response.json().await?; - let keys = json["keys"].as_array().ok_or(AF::msg_err( - "keys", - "required but not found in json web key set", - ))?; - keys.iter() - .find(|value| value["kid"].as_str().eq(&Some(kid))) + let key_set: JwksKeySet = response.json().await?; + let jwks_key = key_set + .keys + .iter() + .find(|key| key.kid.eq(kid)) .cloned() - .ok_or(AF::msg_err("kid", "no match found in json web keys")) - .map_err(AuthExecutionError::from) + .ok_or(AF::msg_err( + "kid", + "no match found for kid in json web keys", + )) + .map_err(AuthExecutionError::from)?; + + if self.app_config.tenant_id.is_some() { + Ok(id_token.decode( + jwks_key.modulus.as_str(), + jwks_key.exponent.as_str(), + &self.app_config.client_id.to_string(), + Some(self.issuer().map_err(AuthorizationFailure::from)?.as_str()), + )?) + } else { + Ok(id_token.decode( + jwks_key.modulus.as_str(), + jwks_key.exponent.as_str(), + &self.app_config.client_id.to_string(), + None, + )?) + } } - pub fn issuer(&self) -> Result<Url, ParseError> { - self.app_config - .azure_cloud_instance - .issuer(&self.app_config.authority) + async fn verify_authorization_id_token_async( + &mut self, + ) -> Option<AuthExecutionResult<TokenData<Claims>>> { + if let Some(id_token) = self.app_config.id_token.as_ref() { + let mut id_token_clone = id_token.clone(); + if !id_token_clone.verified { + return match self.verify_jwks_from_token_async(&mut id_token_clone).await { + Ok(token_data) => { + self.app_config.with_id_token(id_token_clone); + Some(Ok(token_data)) + } + Err(err) => Some(Err(err)), + }; + // return Some(self.verify_jwks_from_token_async(&mut id_token_clone).await) + } + } + None } fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { @@ -208,7 +323,28 @@ impl OpenIdCredential { } let new_token: Token = response.json()?; - self.token_cache.store(cache_id, new_token.clone()); + + if self.verify_id_token { + if let Some(mut id_token) = new_token.id_token.clone() { + tracing::debug!(target: CREDENTIAL_EXECUTOR, "performing jwks verification"); + + let id_token_verification_result = self.verify_jwks_from_token(&mut id_token); + if let Ok(token_data) = id_token_verification_result { + self.id_token_jwt = Some(DecodedJwt::from(token_data)); + dbg!(&self.id_token_jwt); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); + } else if let Err(err) = id_token_verification_result { + tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification failed - evicting token from cache"); + + // The new token has not been stored in the cache but we still need evict any previous tokens. + self.refresh_token = None; + self.token_cache.evict(cache_id.as_str()); + return Err(err); + } + } + } + + self.token_cache.store(cache_id.clone(), new_token.clone()); if new_token.refresh_token.is_some() { self.refresh_token = new_token.refresh_token.clone(); @@ -231,11 +367,37 @@ impl OpenIdCredential { let new_token: Token = response.json().await?; + if self.verify_id_token { + if let Some(mut id_token) = new_token.id_token.clone() { + tracing::debug!( + target: CREDENTIAL_EXECUTOR, + verify_id_token = self.verify_id_token, + "performing jwks verification:" + ); + + let id_token_verification_result = + self.verify_jwks_from_token_async(&mut id_token).await; + if let Ok(token_data) = id_token_verification_result { + self.id_token_jwt = Some(DecodedJwt::from(token_data)); + dbg!(&self.id_token_jwt); + tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); + } else if let Err(err) = id_token_verification_result { + tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification failed - evicting token from cache"); + + // The new token has not been stored in the cache but we still need evict any previous tokens. + self.refresh_token = None; + self.token_cache.evict(cache_id.as_str()); + return Err(err); + } + } + } + + self.token_cache.store(cache_id, new_token.clone()); + if new_token.refresh_token.is_some() { self.refresh_token = new_token.refresh_token.clone(); } - self.token_cache.store(cache_id, new_token.clone()); Ok(new_token) } } @@ -324,6 +486,10 @@ impl TokenCache for OpenIdCredential { fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { self.app_config.force_token_refresh = force_token_refresh; } + + fn decoded_jwt(&self) -> Option<&DecodedJwt> { + self.id_token_jwt.as_ref() + } } #[async_trait] @@ -459,6 +625,8 @@ impl OpenIdCredentialBuilder { serializer: Default::default(), token_cache: Default::default(), openid_config: None, + verify_id_token: Default::default(), + id_token_jwt: None, }, } } @@ -476,6 +644,8 @@ impl OpenIdCredentialBuilder { serializer: Default::default(), token_cache: Default::default(), openid_config: None, + verify_id_token: Default::default(), + id_token_jwt: None, }, } } @@ -483,6 +653,7 @@ impl OpenIdCredentialBuilder { pub(crate) fn new_with_auth_code( mut app_config: AppConfig, authorization_code: impl AsRef<str>, + verify_id_token: bool, ) -> OpenIdCredentialBuilder { app_config.scope.insert("openid".to_string()); OpenIdCredentialBuilder { @@ -496,6 +667,8 @@ impl OpenIdCredentialBuilder { serializer: Default::default(), token_cache: Default::default(), openid_config: None, + verify_id_token, + id_token_jwt: None, }, } } @@ -517,6 +690,8 @@ impl OpenIdCredentialBuilder { serializer: Default::default(), token_cache: Default::default(), openid_config: None, + verify_id_token: Default::default(), + id_token_jwt: None, }, } } @@ -537,6 +712,8 @@ impl OpenIdCredentialBuilder { serializer: Default::default(), token_cache, openid_config: None, + verify_id_token: Default::default(), + id_token_jwt: None, }, } } @@ -582,6 +759,11 @@ impl OpenIdCredentialBuilder { Ok(self) } + pub fn with_id_token_verification(&mut self, verify_id_token: bool) -> &mut Self { + self.credential.verify_id_token = verify_id_token; + self + } + pub fn issuer(&self) -> Result<Url, ParseError> { self.credential.issuer() } @@ -602,12 +784,8 @@ impl OpenIdCredentialBuilder { self.credential.get_jwks_async().await } - pub fn get_jwks_key(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { - self.credential.get_jwks_key(kid) - } - - pub async fn get_jwks_key_async(&self, kid: &str) -> AuthExecutionResult<serde_json::Value> { - self.credential.get_jwks_key_async(kid).await + pub(crate) async fn verify_jwks_async(&self) -> AuthExecutionResult<TokenData<Claims>> { + self.credential.verify_jwks_async().await } pub fn credential(&self) -> &OpenIdCredential { @@ -629,13 +807,23 @@ impl From<OpenIdCredential> for OpenIdCredentialBuilder { impl From<(AppConfig, AuthorizationResponse)> for OpenIdCredentialBuilder { fn from(value: (AppConfig, AuthorizationResponse)) -> Self { - let (app_config, authorization_response) = value; + let (mut app_config, authorization_response) = value; if let Some(authorization_code) = authorization_response.code.as_ref() { - OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code) + if let Some(id_token) = authorization_response.id_token.as_ref() { + app_config.with_id_token(IdToken::new( + id_token.as_ref(), + None, + Some(authorization_code.as_ref()), + None, + )); + OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code, true) + } else { + OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code, false) + } } else { OpenIdCredentialBuilder::new_with_token( app_config, - Token::from(authorization_response.clone()), + Token::try_from(authorization_response.clone()).unwrap_or_default(), ) } } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 04fdefa5..7aeb2a97 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -3,9 +3,10 @@ use std::fmt::Debug; use async_trait::async_trait; use dyn_clone::DynClone; +use graph_core::identity::DecodedJwt; use reqwest::header::HeaderMap; use reqwest::tls::Version; -use url::Url; +use url::{ParseError, Url}; use uuid::Uuid; use graph_error::{AuthExecutionResult, IdentityResult}; @@ -131,6 +132,10 @@ pub trait TokenCredentialExecutor: DynClone + Debug { &self.app_config().extra_header_parameters } + fn issuer(&self) -> Result<Url, ParseError> { + self.azure_cloud_instance().issuer(&self.authority()) + } + fn extra_query_parameters(&self) -> &HashMap<String, String> { &self.app_config().extra_query_parameters } diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index ca436cad..675b212c 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -2,58 +2,19 @@ use serde::de::{Error, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::fmt::{Debug, Display, Formatter}; -use base64::{DecodeError, Engine}; +use crate::identity::AuthorizationResponse; +use base64::Engine; +use graph_core::identity::{Claims, DecodedJwt}; +use graph_error::{AuthorizationFailure, AF}; use jsonwebtoken::errors as JwtErrors; -use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation}; use std::str::FromStr; use url::form_urlencoded::parse; -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct JwtHeader { - pub typ: String, - pub alg: String, - pub kid: String, - pub x5t: Option<String>, -} - -impl Display for JwtHeader { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "typ: {}, alg: {}, kid: {}, x5t: {:#?}", - self.typ, self.alg, self.kid, self.x5t - ) - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub struct Claims { - pub aud: String, - pub iss: String, - pub iat: usize, - pub nbf: usize, - pub exp: usize, - pub aio: String, - pub c_hash: String, - pub cc: String, - pub email: String, - pub name: String, - pub nonce: String, - pub oid: String, - pub preferred_username: String, - pub rh: String, - pub sub: String, - pub tid: String, - pub uti: String, - pub ver: String, - #[serde(flatten)] - pub additional_fields: HashMap<String, Value>, -} - /// ID tokens are sent to the client application as part of an OpenID Connect flow. /// They can be sent alongside or instead of an access token. ID tokens are used by the /// client to authenticate the user. To learn more about how the Microsoft identity @@ -68,6 +29,26 @@ pub struct IdToken { pub additional_fields: HashMap<String, Value>, #[serde(skip)] log_pii: bool, + #[serde(skip)] + pub(crate) verified: bool, +} + +impl TryFrom<AuthorizationResponse> for IdToken { + type Error = AuthorizationFailure; + + fn try_from(value: AuthorizationResponse) -> Result<Self, Self::Error> { + Ok(IdToken { + code: value.code, + id_token: value + .id_token + .ok_or_else(|| AF::msg_err("id_token", "id_token is None"))?, + state: value.state, + session_state: value.session_state, + additional_fields: Default::default(), + log_pii: false, + verified: false, + }) + } } impl IdToken { @@ -84,6 +65,7 @@ impl IdToken { session_state: session_state.map(|value| value.into()), additional_fields: Default::default(), log_pii: false, + verified: false, } } @@ -103,38 +85,34 @@ impl IdToken { /// Decode the id token header. pub fn decode_header(&self) -> JwtErrors::Result<jsonwebtoken::Header> { - /* - let parts: Vec<&str> = self.id_token.split('.').collect(); - if parts.is_empty() { - return Err(JwtErrors::Error::from(JwtErrors::ErrorKind::InvalidToken)); - } - let header_decoded = base64::engine::general_purpose::STANDARD_NO_PAD.decode(parts[0])?; - let utf8_header = String::from_utf8(header_decoded)?; - let jwt_header: JwtHeader = serde_json::from_str(&utf8_header)?; - */ - jsonwebtoken::decode_header(self.id_token.as_str()) } - /// Decode and validate the id token. + /// Decode and verify the id token using the following parameters: + /// modulus (n): product of two prime numbers used to generate key pair. + /// Exponent (e): exponent used to decode the data. + /// client_id: tenant client id in Azure. + /// issuer: issuer for tenant in Azure. pub fn decode( - &self, - n: &str, - e: &str, + &mut self, + modulus: &str, + exponent: &str, client_id: &str, issuer: Option<&str>, - ) -> JwtErrors::Result<TokenData<Claims>> { + ) -> JwtErrors::Result<DecodedJwt> { let mut validation = Validation::new(Algorithm::RS256); validation.set_audience(&[client_id]); if let Some(issuer) = issuer { validation.set_issuer(&[issuer]); } - jsonwebtoken::decode::<Claims>( + let token_data = jsonwebtoken::decode::<Claims>( &self.id_token, - &DecodingKey::from_rsa_components(n, e).unwrap(), + &DecodingKey::from_rsa_components(modulus, exponent).unwrap(), &validation, - ) + )?; + self.verified = true; + Ok(token_data) } /// Enable or disable logging of personally identifiable information such diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index f0c20e88..af68c5f6 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -8,7 +8,6 @@ mod credentials; mod device_authorization_response; mod id_token; mod token; -mod token_validator; #[cfg(feature = "openssl")] pub use openssl::{ @@ -26,4 +25,3 @@ pub use credentials::*; pub use device_authorization_response::*; pub use id_token::*; pub use token::*; -pub use token_validator::*; diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 86bb33ed..c56115fd 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -1,13 +1,14 @@ -use graph_error::GraphFailure; +use graph_error::{AuthorizationFailure, GraphFailure, AF}; use serde::{Deserialize, Deserializer}; use serde_aux::prelude::*; use serde_json::Value; use std::collections::HashMap; use std::fmt; +use std::fmt::{format, Display}; use std::ops::{Add, Sub}; -use crate::identity::{Authority, AuthorizationResponse, Claims, IdToken}; -use graph_core::cache::AsBearer; +use crate::identity::{Authority, AuthorizationResponse, IdToken}; +use graph_core::{cache::AsBearer, identity::Claims}; use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation}; use std::str::FromStr; use time::OffsetDateTime; @@ -41,6 +42,8 @@ struct PhantomToken { user_id: Option<String>, id_token: Option<String>, state: Option<String>, + session_state: Option<String>, + nonce: Option<String>, correlation_id: Option<String>, client_info: Option<String>, #[serde(flatten)] @@ -110,6 +113,8 @@ pub struct Token { pub user_id: Option<String>, pub id_token: Option<IdToken>, pub state: Option<String>, + pub session_state: Option<String>, + pub nonce: Option<String>, pub correlation_id: Option<String>, pub client_info: Option<String>, pub timestamp: Option<time::OffsetDateTime>, @@ -141,6 +146,8 @@ impl Token { user_id: None, id_token: None, state: None, + session_state: None, + nonce: None, correlation_id: None, client_info: None, timestamp: Some(timestamp), @@ -423,6 +430,8 @@ impl Default for Token { user_id: None, id_token: None, state: None, + session_state: None, + nonce: None, correlation_id: None, client_info: None, timestamp: Some(time::OffsetDateTime::now_utc()), @@ -435,33 +444,39 @@ impl Default for Token { } } -impl From<AuthorizationResponse> for Token { - fn from(value: AuthorizationResponse) -> Self { - Token { - access_token: value.access_token.unwrap_or_default(), +impl TryFrom<AuthorizationResponse> for Token { + type Error = AuthorizationFailure; + + fn try_from(value: AuthorizationResponse) -> Result<Self, Self::Error> { + let id_token = IdToken::try_from(value.clone()).ok(); + + Ok(Token { + access_token: value + .access_token + .ok_or_else(|| AF::msg_err("access_token", "access_token is None"))?, token_type: "Bearer".to_string(), - expires_in: 3600, + expires_in: value.expires_in.unwrap_or_default(), ext_expires_in: None, scope: vec![], refresh_token: None, user_id: None, - id_token: value - .id_token - .map(|id_token| IdToken::new(id_token.as_ref(), None, None, None)), - state: None, + id_token, + state: value.state, + session_state: value.session_state, + nonce: value.nonce, correlation_id: None, client_info: None, timestamp: None, expires_on: None, additional_fields: Default::default(), log_pii: false, - } + }) } } -impl ToString for Token { - fn to_string(&self) -> String { - self.access_token.to_string() +impl Display for Token { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.access_token.to_string()) } } @@ -562,19 +577,13 @@ impl<'de> Deserialize<'de> for Token { D: Deserializer<'de>, { let phantom_access_token: PhantomToken = Deserialize::deserialize(deserializer)?; - let timestamp = OffsetDateTime::now_utc(); let expires_on = timestamp.add(time::Duration::seconds(phantom_access_token.expires_in)); + let id_token = phantom_access_token + .id_token + .map(|id_token_string| IdToken::new(id_token_string.as_ref(), None, None, None)); - let id_token = { - if let Some(id_token_string) = phantom_access_token.id_token.as_ref() { - IdToken::from_str(id_token_string.as_ref()).ok() - } else { - None - } - }; - - Ok(Token { + let token = Token { access_token: phantom_access_token.access_token, token_type: phantom_access_token.token_type, expires_in: phantom_access_token.expires_in, @@ -584,13 +593,19 @@ impl<'de> Deserialize<'de> for Token { user_id: phantom_access_token.user_id, id_token, state: phantom_access_token.state, + session_state: phantom_access_token.session_state, + nonce: phantom_access_token.nonce, correlation_id: phantom_access_token.correlation_id, client_info: phantom_access_token.client_info, timestamp: Some(timestamp), expires_on: Some(expires_on), additional_fields: phantom_access_token.additional_fields, log_pii: false, - }) + }; + + // tracing::debug!(target: "phantom", token.as_value()); + + Ok(token) } } @@ -640,4 +655,37 @@ mod test { let _token: Token = serde_json::from_str(ACCESS_TOKEN_INT).unwrap(); let _token: Token = serde_json::from_str(ACCESS_TOKEN_STRING).unwrap(); } + + #[test] + pub fn try_from_url_authorization_response() { + let authorization_response = AuthorizationResponse { + code: Some("code".into()), + id_token: Some("id_token".into()), + expires_in: Some(3600), + access_token: Some("token".into()), + state: Some("state".into()), + session_state: Some("session_state".into()), + nonce: None, + error: None, + error_description: None, + error_uri: None, + additional_fields: Default::default(), + log_pii: false, + }; + + let token = Token::try_from(authorization_response).unwrap(); + assert_eq!( + token.id_token, + Some(IdToken::new( + "id_token", + Some("code"), + Some("state"), + Some("session_state") + )) + ); + assert_eq!(token.access_token, "token".to_string()); + assert_eq!(token.state, Some("state".to_string())); + assert_eq!(token.session_state, Some("session_state".to_string())); + assert_eq!(token.expires_in, 3600); + } } diff --git a/graph-oauth/src/identity/token_validator.rs b/graph-oauth/src/identity/token_validator.rs deleted file mode 100644 index 41a2dd4f..00000000 --- a/graph-oauth/src/identity/token_validator.rs +++ /dev/null @@ -1,16 +0,0 @@ -#[derive(Clone, Default)] -pub struct TokenValidator { - application_id: Option<String>, -} - -impl TokenValidator { - pub fn builder() -> TokenValidator { - TokenValidator::default() - } - - // Validate the audience - pub fn with_aud(&mut self, aud: impl AsRef<str>) -> &mut Self { - self.application_id = Some(aud.as_ref().to_owned()); - self - } -} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 8214bede..a1d516b7 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -1,35 +1,21 @@ -//! # OAuth client implementing the OAuth 2.0 and OpenID Connect protocols on Microsoft identity platform +//! # Microsoft Identity Platform Client //! -//! Purpose built as OAuth client for Microsoft Graph and the [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) project. -//! This project can however be used outside [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) as an OAuth client -//! for Microsoft Identity Platform. +//! Support For OAuth 2.0 and OpenId authorization flows from the Microsoft Identity Platform. //! -//! ### Supported Authorization Flows +//! Part of the [graph-rs-sdk](https://crates.io/crates/graph-rs-sdk) project on [GitHub](https://crates.io/crates/graph-rs-sdk) //! -//! #### Microsoft Identity Platform -//! -//! - [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) -//! - [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) -//! - [Authorization Code Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) -//! - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) -//! - [Implicit Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) -//! - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) -//! - [Client Credentials - Client Secret](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#first-case-access-token-request-with-a-shared-secret) -//! - [Client Credentials - Client Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) -//! - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) //! //! # Example ConfidentialClientApplication Authorization Code Flow //! ```rust //! use url::Url; -//! use graph_error::IdentityResult; //! use graph_oauth::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! -//! pub fn authorization_url(client_id: &str) -> IdentityResult<Url> { -//! ConfidentialClientApplication::builder(client_id) +//! pub fn authorization_url(client_id: &str) -> anyhow::Result<Url> { +//! Ok(ConfidentialClientApplication::builder(client_id) //! .auth_code_url_builder() -//! .with_redirect_uri("http://localhost:8000/redirect") +//! .with_redirect_uri(Url::parse("http://localhost:8000/redirect")?) //! .with_scope(vec!["user.read"]) -//! .url() +//! .url()?) //! } //! //! pub fn get_confidential_client(authorization_code: &str, client_id: &str, client_secret: &str) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { @@ -37,10 +23,21 @@ //! .with_auth_code(authorization_code) //! .with_client_secret(client_secret) //! .with_scope(vec!["user.read"]) -//! .with_redirect_uri("http://localhost:8000/redirect")? +//! .with_redirect_uri(Url::parse("http://localhost:8000/redirect")?) //! .build()) //! } //! ``` +//! #### Supported Authorization Flows From The Microsoft Identity Platform +//! +//! - [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +//! - [Authorization Code Grant PKCE](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +//! - [Authorization Code Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-access-token-with-a-certificate-credential) +//! - [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) +//! - [Implicit Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-implicit-grant-flow) +//! - [Device Code Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code) +//! - [Client Credentials - Client Secret](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#first-case-access-token-request-with-a-shared-secret) +//! - [Client Credentials - Client Certificate](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate) +//! - [Resource Owner Password Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc) #[macro_use] extern crate serde; @@ -54,7 +51,7 @@ pub(crate) mod oauth_serializer; pub(crate) mod identity; #[cfg(feature = "interactive-auth")] -pub(crate) mod web; +pub mod web; pub(crate) mod internal { pub use crate::oauth_serializer::*; @@ -65,13 +62,6 @@ pub mod extensions { pub use crate::oauth_serializer::*; } -pub mod oauth { - pub use graph_core::{crypto::GenPkce, crypto::ProofKeyCodeExchange}; - - pub use crate::identity::*; - - #[cfg(feature = "interactive-auth")] - pub mod web { - pub use crate::web::*; - } -} +pub use crate::identity::*; +pub use graph_core::{crypto::GenPkce, crypto::ProofKeyCodeExchange}; +pub use jsonwebtoken::{Header, TokenData}; diff --git a/graph-oauth/src/web/interactive_auth.rs b/graph-oauth/src/web/interactive_auth.rs index 19e91c7e..6a5520cd 100644 --- a/graph-oauth/src/web/interactive_auth.rs +++ b/graph-oauth/src/web/interactive_auth.rs @@ -12,6 +12,9 @@ use wry::webview::WebView; #[cfg(target_family = "unix")] use wry::application::platform::unix::EventLoopBuilderExtUnix; +use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationEvent}; +use crate::Secret; +use graph_error::WebViewResult; #[cfg(target_family = "windows")] use wry::application::platform::windows::EventLoopBuilderExtWindows; @@ -196,3 +199,23 @@ where .build() } } + +pub trait WithInteractiveAuth<T> { + type CredentialBuilder: Clone + Debug; + + fn with_interactive_auth( + &self, + auth_type: T, + options: WebViewOptions, + ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>>; +} + +pub trait WithInteractiveAuthBuilder<CredentialBuilder: Clone + Debug> { + type AuthType; + + fn with_interactive_auth_builder( + &self, + auth_type: Self::AuthType, + options: WebViewOptions, + ) -> WebViewResult<AuthorizationEvent<CredentialBuilder>>; +} diff --git a/graph-oauth/src/web/webview_options.rs b/graph-oauth/src/web/webview_options.rs index 165520fa..c2481ff8 100644 --- a/graph-oauth/src/web/webview_options.rs +++ b/graph-oauth/src/web/webview_options.rs @@ -56,7 +56,7 @@ pub struct WebViewOptions { /// sign in the user is automatically logged in through SSO. Or you can clear the browsing /// data, cookies in this case, after sign in when the webview window closes. /// - /// Default is true + /// Default is false pub clear_browsing_data: bool, } @@ -66,7 +66,7 @@ impl WebViewOptions { } /// Give the window a title. The default is "Sign In" - pub fn with_window_title(mut self, window_title: impl ToString) -> Self { + pub fn window_title(mut self, window_title: impl ToString) -> Self { self.window_title = window_title.to_string(); self } @@ -74,12 +74,12 @@ impl WebViewOptions { /// OS specific theme. Only available on Windows. /// See wry crate for more info. #[cfg(windows)] - pub fn with_theme(mut self, theme: Theme) -> Self { + pub fn theme(mut self, theme: Theme) -> Self { self.theme = Some(theme); self } - pub fn with_ports(mut self, ports: HashSet<usize>) -> Self { + pub fn ports(mut self, ports: HashSet<usize>) -> Self { self.ports = ports; self } @@ -88,7 +88,7 @@ impl WebViewOptions { /// when that timeout is reached. For instance, if your app is waiting on the /// user to log in and the user has not logged in after 20 minutes you may /// want to assume the user is idle in some way and close out of the webview window. - pub fn with_timeout(mut self, instant: Instant) -> Self { + pub fn timeout(mut self, instant: Instant) -> Self { self.timeout = Some(instant); self } @@ -96,7 +96,7 @@ impl WebViewOptions { /// The webview can store the cookies that were set after sign in so that on the next /// sign in the user is automatically logged in through SSO. Or you can clear the browsing /// data, cookies in this case, after sign in when the webview window closes. - pub fn with_clear_browsing_data(mut self, clear_browsing_data: bool) -> Self { + pub fn clear_browsing_data_on_close(mut self, clear_browsing_data: bool) -> Self { self.clear_browsing_data = clear_browsing_data; self } @@ -110,7 +110,7 @@ impl Default for WebViewOptions { theme: None, ports: Default::default(), timeout: None, - clear_browsing_data: true, + clear_browsing_data: Default::default(), } } } @@ -122,7 +122,7 @@ impl Default for WebViewOptions { window_title: "Sign In".to_string(), ports: Default::default(), timeout: None, - clear_browsing_data: true, + clear_browsing_data: Default::default(), } } } diff --git a/src/client/graph.rs b/src/client/graph.rs index 8a4a9143..d851bc93 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -40,18 +40,18 @@ use crate::group_lifecycle_policies::{ GroupLifecyclePoliciesApiClient, GroupLifecyclePoliciesIdApiClient, }; use crate::groups::{GroupsApiClient, GroupsIdApiClient}; -use crate::identity::IdentityApiClient; -use crate::identity_governance::IdentityGovernanceApiClient; -use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; -use crate::invitations::InvitationsApiClient; -use crate::me::MeApiClient; -use crate::oauth::{ +use crate::identity::{ AllowedHostValidator, AuthorizationCodeAssertionCredential, AuthorizationCodeCertificateCredential, AuthorizationCodeCredential, BearerTokenCredential, ClientAssertionCredential, ClientCertificateCredential, ClientSecretCredential, ConfidentialClientApplication, DeviceCodeCredential, HostIs, OpenIdCredential, PublicClientApplication, ResourceOwnerPasswordCredential, Token, }; +use crate::identity_access::IdentityApiClient; +use crate::identity_governance::IdentityGovernanceApiClient; +use crate::identity_providers::{IdentityProvidersApiClient, IdentityProvidersIdApiClient}; +use crate::invitations::InvitationsApiClient; +use crate::me::MeApiClient; use crate::oauth2_permission_grants::{ Oauth2PermissionGrantsApiClient, Oauth2PermissionGrantsIdApiClient, }; @@ -71,7 +71,7 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; -use graph_core::identity::ForceTokenRefresh; +use graph_core::identity::{DecodedJwt, ForceTokenRefresh}; use lazy_static::lazy_static; lazy_static! { @@ -296,6 +296,10 @@ impl GraphClient { } } + pub fn decoded_jwt(&self) -> Option<&DecodedJwt> { + self.client.get_decoded_jwt() + } + api_client_impl!(admin, AdminApiClient); api_client_impl!(app_catalogs, AppCatalogsApiClient); diff --git a/src/identity/mod.rs b/src/identity_access/mod.rs similarity index 100% rename from src/identity/mod.rs rename to src/identity_access/mod.rs diff --git a/src/identity/request.rs b/src/identity_access/request.rs similarity index 100% rename from src/identity/request.rs rename to src/identity_access/request.rs diff --git a/src/lib.rs b/src/lib.rs index 4b4d0c21..0f64c76d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,7 @@ pub mod education; pub mod extended_properties; pub mod group_lifecycle_policies; pub mod groups; -pub mod identity; +pub mod identity_access; pub mod identity_governance; pub mod identity_providers; pub mod invitations; @@ -234,9 +234,9 @@ pub use graph_error::{GraphFailure, GraphResult}; pub use graph_http::api_impl::{GraphClientConfiguration, ODataQuery}; /// Reexport of graph-oauth crate. -pub mod oauth { +pub mod identity { pub use graph_core::identity::ClientApplication; - pub use graph_oauth::oauth::*; + pub use graph_oauth::*; } pub mod http { @@ -246,13 +246,15 @@ pub mod http { AsyncIterator, ODataDeltaLink, ODataDownloadLink, ODataMetadataLink, ODataNextLink, ODataQuery, ResponseBlockingExt, ResponseExt, UploadSessionLink, }; - pub use reqwest::tls::Version; - pub use reqwest::{Body, Method}; pub mod blocking { pub use graph_http::api_impl::UploadSessionBlocking; pub use reqwest::blocking::Body; } + + pub use reqwest::tls::Version; + pub use reqwest::{Body, Method}; + pub use url::Url; } /// Reexport of graph-error crate. @@ -267,13 +269,13 @@ pub mod header { pub(crate) mod api_default_imports { pub(crate) use handlebars::*; - pub use reqwest::Method; - pub use url::Url; + pub(crate) use reqwest::Method; + pub(crate) use url::Url; - pub use graph_core::resource::ResourceIdentity; - pub use graph_error::*; + pub(crate) use graph_core::resource::ResourceIdentity; + pub(crate) use graph_error::*; pub(crate) use graph_http::api_impl::*; - pub use crate::client::Graph; + pub(crate) use crate::client::Graph; pub(crate) use crate::client::{map_errors, map_parameters, ResourceProvisioner}; } diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index f6607be2..0cd1ada4 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -2,7 +2,7 @@ use from_as::*; use graph_core::resource::ResourceIdentity; -use graph_rs_sdk::oauth::{ +use graph_rs_sdk::identity::{ ClientSecretCredential, ConfidentialClientApplication, ResourceOwnerPasswordCredential, Token, TokenCredentialExecutor, }; From e17217fd69f7422c5a8440cfe5c3690a1bfec42f Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 24 Dec 2023 07:32:45 -0500 Subject: [PATCH 074/118] Fix build errors --- .../authorization_sign_in/auth_code_grant.rs | 6 ++-- .../client_credentials.rs | 6 ++-- examples/authorization_sign_in/main.rs | 5 +-- .../authorization_sign_in/openid_connect.rs | 8 ++--- .../auth_code_grant/server_example/mod.rs | 6 ++-- .../auth_code_grant/auth_code_grant_pkce.rs | 4 +-- .../server_examples/auth_code_grant_pkce.rs | 6 ++-- .../server_examples/auth_code_grant_secret.rs | 6 ++-- .../getting_tokens_manually.rs | 1 - examples/identity_platform_auth/main.rs | 4 +-- .../openid/server_examples/openid.rs | 4 +-- .../auth_code_authorization_url.rs | 7 ++-- ...thorization_code_certificate_credential.rs | 11 +++---- .../authorization_code_credential.rs | 19 ++++++----- .../client_certificate_credential.rs | 1 + .../credentials/device_code_credential.rs | 12 +++---- graph-oauth/src/identity/credentials/mod.rs | 2 ++ .../credentials/open_id_authorization_url.rs | 33 ++++++++----------- .../credentials/open_id_credential.rs | 18 ++++------ .../credentials/token_credential_executor.rs | 2 +- graph-oauth/src/identity/id_token.rs | 4 +-- graph-oauth/src/web/interactive_auth.rs | 4 +-- graph-oauth/src/web/mod.rs | 4 ++- src/lib.rs | 1 - 24 files changed, 83 insertions(+), 91 deletions(-) diff --git a/examples/authorization_sign_in/auth_code_grant.rs b/examples/authorization_sign_in/auth_code_grant.rs index 977a6f75..82602b0b 100644 --- a/examples/authorization_sign_in/auth_code_grant.rs +++ b/examples/authorization_sign_in/auth_code_grant.rs @@ -7,13 +7,13 @@ use url::Url; static CLIENT_ID: &str = "<CLIENT_ID>"; static CLIENT_SECRET: &str = "<CLIENT_SECRET>"; -static REDIRECT_URI: Url = Url::parse("http://localhost:8000/redirect").unwrap(); +const REDIRECT_URI: &str = "http://localhost:8000/redirect"; static SCOPE: &str = "User.Read"; // or pass more values to vec![] below // Authorization Code Grant Auth URL Builder pub fn auth_code_grant_authorization() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -42,7 +42,7 @@ fn auth_code_grant_pkce_authorization() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_pkce(&pkce) .url() .unwrap(); diff --git a/examples/authorization_sign_in/client_credentials.rs b/examples/authorization_sign_in/client_credentials.rs index 4f4dc76b..b3effb47 100644 --- a/examples/authorization_sign_in/client_credentials.rs +++ b/examples/authorization_sign_in/client_credentials.rs @@ -1,5 +1,7 @@ -use graph_oauth::oauth::ClientSecretCredential; -use graph_rs_sdk::{error::IdentityResult, identity::ClientCredentialsAuthorizationUrlParameters}; +use graph_rs_sdk::{ + error::IdentityResult, + identity::{ClientCredentialsAuthorizationUrlParameters, ClientSecretCredential}, +}; // The client_id must be changed before running this example. static CLIENT_ID: &str = "<CLIENT_ID>"; diff --git a/examples/authorization_sign_in/main.rs b/examples/authorization_sign_in/main.rs index fdd92afb..eb98b544 100644 --- a/examples/authorization_sign_in/main.rs +++ b/examples/authorization_sign_in/main.rs @@ -19,6 +19,7 @@ use graph_rs_sdk::identity::{ DeviceCodeCredential, GenPkce, ProofKeyCodeExchange, PublicClientApplication, Token, TokenCredentialExecutor, }; +use url::Url; fn main() {} @@ -30,7 +31,7 @@ static SCOPE: &str = "User.Read"; // or pass more values to vec![] below // Authorization Code Grant Auth URL Builder pub fn auth_code_grant_authorization() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -59,7 +60,7 @@ fn auth_code_grant_pkce_authorization() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_pkce(&pkce) .url() .unwrap(); diff --git a/examples/authorization_sign_in/openid_connect.rs b/examples/authorization_sign_in/openid_connect.rs index 4635c7e0..d0827d8e 100644 --- a/examples/authorization_sign_in/openid_connect.rs +++ b/examples/authorization_sign_in/openid_connect.rs @@ -33,7 +33,7 @@ fn openid_authorization_url3( OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) //.with_default_scope()? - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(Url::parse(redirect_uri)?) .with_response_mode(ResponseMode::FormPost) .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) @@ -54,7 +54,7 @@ fn map_to_credential( let auth_url_builder = OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) //.with_default_scope()? - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(Url::parse(redirect_uri)?) .with_response_mode(ResponseMode::FormPost) .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) @@ -86,7 +86,7 @@ fn auth_url_using_confidential_client_builder( ConfidentialClientApplication::builder(client_id) .openid_url_builder() .with_tenant(tenant) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(Url::parse(redirect_uri)?) .with_scope(scope) .build() .url() @@ -101,7 +101,7 @@ fn auth_url_using_open_id_credential( ) -> IdentityResult<Url> { OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(Url::parse(redirect_uri)?) .with_scope(scope) .build() .url() diff --git a/examples/certificate_auth/auth_code_grant/server_example/mod.rs b/examples/certificate_auth/auth_code_grant/server_example/mod.rs index edae0710..f4a61048 100644 --- a/examples/certificate_auth/auth_code_grant/server_example/mod.rs +++ b/examples/certificate_auth/auth_code_grant/server_example/mod.rs @@ -37,7 +37,7 @@ static CLIENT_ID: &str = "<CLIENT_ID>"; // Only required for certain applications. Used here as an example. static TENANT: &str = "<TENANT_ID>"; -static REDIRECT_URI: Url = Url::parse("http://localhost:8000/redirect").unwrap(); +static REDIRECT_URI: &str = "http://localhost:8000/redirect"; static SCOPE: &str = "User.Read"; @@ -55,7 +55,7 @@ pub struct AccessCode { pub fn authorization_sign_in() { let url = AuthorizationCodeCertificateCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT) - .with_redirect_uri(REDIRECT_URI.clone()) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -89,7 +89,7 @@ fn build_confidential_client( .with_auth_code_x509_certificate(authorization_code, &x509certificate)? .with_tenant(TENANT) .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI.clone())? + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .build()) } diff --git a/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs index abd1e116..242ee3ce 100644 --- a/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs +++ b/examples/identity_platform_auth/auth_code_grant/auth_code_grant_pkce.rs @@ -31,7 +31,7 @@ fn authorization_sign_in_url( Ok( AuthorizationCodeCredential::authorization_url_builder(client_id) .with_scope(scope) - .with_redirect_uri(redirect_uri) + .with_redirect_uri(Url::parse(redirect_uri).unwrap()) .with_pkce(&PKCE) .url()?, ) @@ -48,7 +48,7 @@ fn build_confidential_client( .with_auth_code(authorization_code) .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(Url::parse(redirect_uri).unwrap()) .with_pkce(&PKCE) .build()) } diff --git a/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs index bc00f51e..7b645857 100644 --- a/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs +++ b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_pkce.rs @@ -4,6 +4,7 @@ use graph_rs_sdk::identity::{ ResponseType, Token, TokenCredentialExecutor, }; use lazy_static::lazy_static; +use url::Url; use warp::{get, Filter}; static CLIENT_ID: &str = "<CLIENT_ID>"; @@ -34,7 +35,7 @@ pub struct AccessCode { fn authorization_sign_in() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) .with_scope(vec!["user.read"]) - .with_redirect_uri("http://localhost:8000/redirect") + .with_redirect_uri(Url::parse("http://localhost:8000/redirect").unwrap()) .with_pkce(&PKCE) .url() .unwrap(); @@ -58,8 +59,7 @@ async fn handle_redirect( let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) .with_auth_code(authorization_code) .with_client_secret(CLIENT_SECRET) - .with_redirect_uri("http://localhost:8000/redirect") - .unwrap() + .with_redirect_uri(Url::parse("http://localhost:8000/redirect").unwrap()) .with_pkce(&PKCE) .build(); diff --git a/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs index f38cb1c3..16519f4d 100644 --- a/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs +++ b/examples/identity_platform_auth/auth_code_grant/server_examples/auth_code_grant_secret.rs @@ -4,6 +4,7 @@ use graph_rs_sdk::identity::{ Token, TokenCredentialExecutor, }; use graph_rs_sdk::*; +use url::Url; use warp::Filter; // Update these values with your own or provide them directly in the @@ -20,7 +21,7 @@ pub struct AccessCode { pub fn authorization_sign_in() { let url = AuthorizationCodeCredential::authorization_url_builder(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_scope(vec![SCOPE]) .url() .unwrap(); @@ -34,8 +35,7 @@ fn get_graph_client(authorization_code: &str) -> Graph { .with_auth_code(authorization_code) .with_client_secret(CLIENT_SECRET) .with_scope(vec![SCOPE]) - .with_redirect_uri(REDIRECT_URI) - .unwrap() + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .build(); GraphClient::from(&confidential_client) } diff --git a/examples/identity_platform_auth/getting_tokens_manually.rs b/examples/identity_platform_auth/getting_tokens_manually.rs index 047a8f46..823cbc9a 100644 --- a/examples/identity_platform_auth/getting_tokens_manually.rs +++ b/examples/identity_platform_auth/getting_tokens_manually.rs @@ -16,7 +16,6 @@ async fn auth_code_grant( AuthorizationCodeCredential::builder(client_id, client_secret, authorization_code) .with_scope(scope) .with_redirect_uri(Url::parse(redirect_uri).unwrap()) - .unwrap() .build(); let response = confidential_client.execute_async().await.unwrap(); diff --git a/examples/identity_platform_auth/main.rs b/examples/identity_platform_auth/main.rs index 88f9f962..9a0946df 100644 --- a/examples/identity_platform_auth/main.rs +++ b/examples/identity_platform_auth/main.rs @@ -32,6 +32,7 @@ use graph_rs_sdk::identity::{ TokenCredentialExecutor, }; use graph_rs_sdk::GraphClient; +use url::Url; fn main() {} @@ -46,8 +47,7 @@ async fn auth_code_grant( let mut confidential_client = AuthorizationCodeCredential::builder(client_id, client_secret, authorization_code) .with_scope(scope) - .with_redirect_uri(redirect_uri) - .unwrap() + .with_redirect_uri(Url::parse(redirect_uri).unwrap()) .build(); let _graph_client = GraphClient::from(&confidential_client); diff --git a/examples/identity_platform_auth/openid/server_examples/openid.rs b/examples/identity_platform_auth/openid/server_examples/openid.rs index 067f897d..c983188b 100644 --- a/examples/identity_platform_auth/openid/server_examples/openid.rs +++ b/examples/identity_platform_auth/openid/server_examples/openid.rs @@ -34,7 +34,7 @@ fn openid_authorization_url() -> anyhow::Result<Url> { Ok(OpenIdCredential::authorization_url_builder(CLIENT_ID) .with_tenant(TENANT_ID) //.with_default_scope()? - .with_redirect_uri(REDIRECT_URI)? + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_response_mode(ResponseMode::FormPost) .with_response_type([ResponseType::IdToken, ResponseType::Code]) .with_prompt(Prompt::SelectAccount) @@ -64,7 +64,7 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, let mut confidential_client = ConfidentialClientApplication::builder(CLIENT_ID) .with_openid(code, CLIENT_SECRET) .with_tenant(TENANT_ID) - .with_redirect_uri(REDIRECT_URI) + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .unwrap() .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope .build(); diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 83ed3f63..bd281d06 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -295,8 +295,9 @@ impl AuthCodeAuthorizationUrlParameters { } } + #[allow(dead_code)] #[cfg(feature = "interactive-auth")] - pub(crate) fn interactive_authentication_builder<CredentialBuilder: Clone>( + pub(crate) fn interactive_authentication_builder( &self, options: WebViewOptions, ) -> WebViewResult<AuthorizationResponse> { @@ -747,7 +748,7 @@ impl WithInteractiveAuth<Secret> for AuthCodeAuthorizationUrlParameterBuilder { } }; - credential_builder.with_client_secret(auth_type.into_inner()); + credential_builder.with_client_secret(auth_type.0); Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, @@ -788,7 +789,7 @@ impl WithInteractiveAuth<Assertion> for AuthCodeAuthorizationUrlParameterBuilder } }; - credential_builder.with_client_assertion(auth_type.into_inner()); + credential_builder.with_client_assertion(auth_type.0); Ok(AuthorizationEvent::Authorized { authorization_response, credential_builder, diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index 4c0dc094..e1e49707 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -11,15 +11,13 @@ use uuid::Uuid; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; -use graph_error::{ - AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult, AF, -}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; -use crate::identity::credentials::app_config::AppConfig; #[cfg(feature = "openssl")] -use crate::identity::X509Certificate; +use crate::identity::{AuthorizationResponse, X509Certificate}; + use crate::identity::{ - AuthCodeAuthorizationUrlParameterBuilder, Authority, AuthorizationResponse, AzureCloudInstance, + AppConfig, AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; @@ -444,6 +442,7 @@ impl AuthorizationCodeCertificateCredentialBuilder { Ok(builder) } + #[allow(unused)] #[cfg(feature = "openssl")] pub(crate) fn new_authorization_response( value: (AppConfig, AuthorizationResponse, &X509Certificate), diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 2c7908ae..46755151 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -19,7 +19,7 @@ use crate::identity::{ ConfidentialClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; -use crate::{AuthCodeAuthorizationUrlParameterBuilder, Secret}; +use crate::AuthCodeAuthorizationUrlParameterBuilder; credential_builder!( AuthorizationCodeCredentialBuilder, @@ -324,6 +324,7 @@ impl AuthorizationCodeCredentialBuilder { } } + #[allow(dead_code)] pub(crate) fn from_secret( authorization_code: String, secret: String, @@ -533,9 +534,9 @@ mod test { #[test] fn with_tenant_id_common() { let credential = AuthorizationCodeCredential::builder( + "auth_code", Uuid::new_v4().to_string(), - "secret".to_string(), - "code", + "client_secret", ) .with_authority(Authority::TenantId("common".into())) .build(); @@ -546,9 +547,9 @@ mod test { #[test] fn with_tenant_id_adfs() { let credential = AuthorizationCodeCredential::builder( + "auth_code", Uuid::new_v4().to_string(), - "secret".to_string(), - "code", + "client_secret", ) .with_authority(Authority::AzureDirectoryFederatedServices) .build(); @@ -560,9 +561,9 @@ mod test { #[should_panic] fn required_value_missing_client_id() { let mut credential_builder = AuthorizationCodeCredential::builder( + "auth_code", Uuid::default().to_string(), - "secret".to_string(), - "code", + "secret", ); credential_builder .with_authorization_code("code") @@ -575,7 +576,7 @@ mod test { fn serialization() { let uuid_value = Uuid::new_v4().to_string(); let mut credential_builder = - AuthorizationCodeCredential::builder(uuid_value.clone(), "secret".to_string(), "code"); + AuthorizationCodeCredential::builder("auth_code", uuid_value.clone(), "secret"); let mut credential = credential_builder .with_redirect_uri(Url::parse("http://localhost").unwrap()) .with_client_secret("client_secret") @@ -591,7 +592,7 @@ mod test { fn should_force_refresh_test() { let uuid_value = Uuid::new_v4().to_string(); let mut credential_builder = - AuthorizationCodeCredential::builder(uuid_value, "secret".to_string(), "code"); + AuthorizationCodeCredential::builder("auth_code", uuid_value, "client_secret"); let _credential = credential_builder .with_redirect_uri(Url::parse("http://localhost").unwrap()) .with_client_secret("client_secret") diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index 6ada20ed..da08e70d 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -267,6 +267,7 @@ impl ClientCertificateCredentialBuilder { Ok(self) } + #[allow(dead_code)] fn with_client_assertion<T: AsRef<str>>(&mut self, client_assertion: T) -> &mut Self { self.credential.client_assertion = client_assertion.as_ref().to_owned(); self diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 5ebe0350..6690414f 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -571,9 +571,7 @@ impl DeviceCodePollingExecutor { credential: self.credential.clone(), interval: Duration::from_secs(device_authorization_response.interval), verification_uri: device_authorization_response.verification_uri.clone(), - verification_uri_complete: device_authorization_response - .verification_uri_complete - .clone(), + verification_uri_complete: device_authorization_response.verification_uri_complete, }, )) } @@ -623,9 +621,7 @@ impl DeviceCodeInteractiveAuth { credential, interval: Duration::from_secs(device_authorization_response.interval), verification_uri: device_authorization_response.verification_uri.clone(), - verification_uri_complete: device_authorization_response - .verification_uri_complete - .clone(), + verification_uri_complete: device_authorization_response.verification_uri_complete, } } @@ -654,7 +650,7 @@ impl DeviceCodeInteractiveAuth { &mut self, ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let mut credential = self.credential.clone(); - let interval = self.interval.clone(); + let interval = self.interval; let mut should_slow_down = false; @@ -668,7 +664,7 @@ impl DeviceCodeInteractiveAuth { } let response = credential.execute().unwrap(); - let http_response = response.into_http_response().map_err(|err| Box::new(err))?; + let http_response = response.into_http_response().map_err(Box::new)?; let status = http_response.status(); if status.is_success() { diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 3d58ebeb..27115b7e 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -61,6 +61,8 @@ mod x509_certificate; pub(crate) mod tracing_targets { pub const CREDENTIAL_EXECUTOR: &str = "graph_rs_sdk::credential_executor"; + + #[allow(dead_code)] pub const INTERACTIVE_AUTH: &str = "graph_rs_sdk::interactive_auth"; } diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index e79ebfe4..ff2e7e77 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -11,30 +11,25 @@ use graph_error::{AuthorizationFailure, IdentityResult, AF}; use crate::identity::credentials::app_config::AppConfig; use crate::identity::{ - AsQuery, Authority, AuthorizationImpeded, AuthorizationUrl, AzureCloudInstance, - OpenIdCredentialBuilder, Prompt, ResponseMode, ResponseType, + AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, OpenIdCredentialBuilder, Prompt, + ResponseMode, ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; -#[cfg(feature = "interactive-auth")] -use graph_error::{WebViewError, WebViewResult}; - -#[cfg(feature = "interactive-auth")] -use crate::identity::{AuthorizationResponse, Token}; - -#[cfg(feature = "interactive-auth")] -use crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, WebViewHostValidator, WebViewOptions, -}; - use crate::identity::tracing_targets::CREDENTIAL_EXECUTOR; + #[cfg(feature = "interactive-auth")] -use crate::web::UserEvents; -use crate::{AuthorizationEvent, PhantomAuthorizationResponse}; -#[cfg(feature = "interactive-auth")] -use wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, +use { + crate::identity::{AuthorizationEvent, AuthorizationResponse}, + crate::web::{ + HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, + WebViewOptions, + }, + graph_error::{WebViewError, WebViewResult}, + wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, + }, }; const RESPONSE_TYPES_SUPPORTED: &[&str] = &["code", "id_token", "code id_token", "id_token token"]; diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index ce32bf50..64428245 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -4,7 +4,7 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; -use jsonwebtoken::errors::ErrorKind; + use jsonwebtoken::TokenData; use reqwest::IntoUrl; use url::{ParseError, Url}; @@ -68,7 +68,6 @@ pub struct OpenIdCredential { pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: OAuthSerializer, token_cache: InMemoryCacheStore<Token>, - openid_config: Option<serde_json::Value>, verify_id_token: bool, id_token_jwt: Option<DecodedJwt>, } @@ -101,7 +100,6 @@ impl OpenIdCredential { pkce: None, serializer: Default::default(), token_cache: Default::default(), - openid_config: None, verify_id_token: Default::default(), id_token_jwt: None, }) @@ -184,7 +182,6 @@ impl OpenIdCredential { .ok_or(AF::msg_err("token", "no cached token"))?; let mut id_token = token .id_token - .clone() .ok_or(AF::msg_err("id_token", "no cached id_token"))?; self.verify_jwks_from_token(&mut id_token) } @@ -294,6 +291,7 @@ impl OpenIdCredential { } } + #[allow(unused)] async fn verify_authorization_id_token_async( &mut self, ) -> Option<AuthExecutionResult<TokenData<Claims>>> { @@ -330,7 +328,7 @@ impl OpenIdCredential { let id_token_verification_result = self.verify_jwks_from_token(&mut id_token); if let Ok(token_data) = id_token_verification_result { - self.id_token_jwt = Some(DecodedJwt::from(token_data)); + self.id_token_jwt = Some(token_data); dbg!(&self.id_token_jwt); tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); } else if let Err(err) = id_token_verification_result { @@ -344,7 +342,7 @@ impl OpenIdCredential { } } - self.token_cache.store(cache_id.clone(), new_token.clone()); + self.token_cache.store(cache_id, new_token.clone()); if new_token.refresh_token.is_some() { self.refresh_token = new_token.refresh_token.clone(); @@ -378,7 +376,7 @@ impl OpenIdCredential { let id_token_verification_result = self.verify_jwks_from_token_async(&mut id_token).await; if let Ok(token_data) = id_token_verification_result { - self.id_token_jwt = Some(DecodedJwt::from(token_data)); + self.id_token_jwt = Some(token_data); dbg!(&self.id_token_jwt); tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); } else if let Err(err) = id_token_verification_result { @@ -624,7 +622,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - openid_config: None, verify_id_token: Default::default(), id_token_jwt: None, }, @@ -643,7 +640,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - openid_config: None, verify_id_token: Default::default(), id_token_jwt: None, }, @@ -666,7 +662,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - openid_config: None, verify_id_token, id_token_jwt: None, }, @@ -689,7 +684,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - openid_config: None, verify_id_token: Default::default(), id_token_jwt: None, }, @@ -711,7 +705,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache, - openid_config: None, verify_id_token: Default::default(), id_token_jwt: None, }, @@ -784,6 +777,7 @@ impl OpenIdCredentialBuilder { self.credential.get_jwks_async().await } + #[allow(dead_code)] pub(crate) async fn verify_jwks_async(&self) -> AuthExecutionResult<TokenData<Claims>> { self.credential.verify_jwks_async().await } diff --git a/graph-oauth/src/identity/credentials/token_credential_executor.rs b/graph-oauth/src/identity/credentials/token_credential_executor.rs index 7aeb2a97..34c05c1c 100644 --- a/graph-oauth/src/identity/credentials/token_credential_executor.rs +++ b/graph-oauth/src/identity/credentials/token_credential_executor.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use async_trait::async_trait; use dyn_clone::DynClone; -use graph_core::identity::DecodedJwt; + use reqwest::header::HeaderMap; use reqwest::tls::Version; use url::{ParseError, Url}; diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index 675b212c..632d8520 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -2,7 +2,7 @@ use serde::de::{Error, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{Debug, Display, Formatter}; @@ -78,7 +78,7 @@ impl IdToken { let payload_decoded = base64::engine::general_purpose::STANDARD_NO_PAD .decode(parts[1]) .unwrap(); - let utf8_payload = String::from_utf8(payload_decoded)?.to_owned(); + let utf8_payload = String::from_utf8(payload_decoded)?; let payload: serde_json::Value = serde_json::from_str(&utf8_payload)?; Ok(payload) } diff --git a/graph-oauth/src/web/interactive_auth.rs b/graph-oauth/src/web/interactive_auth.rs index 6a5520cd..5e402775 100644 --- a/graph-oauth/src/web/interactive_auth.rs +++ b/graph-oauth/src/web/interactive_auth.rs @@ -12,8 +12,8 @@ use wry::webview::WebView; #[cfg(target_family = "unix")] use wry::application::platform::unix::EventLoopBuilderExtUnix; -use crate::identity::{AuthorizationCodeCredentialBuilder, AuthorizationEvent}; -use crate::Secret; +use crate::identity::AuthorizationEvent; + use graph_error::WebViewResult; #[cfg(target_family = "windows")] use wry::application::platform::windows::EventLoopBuilderExtWindows; diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/web/mod.rs index d4aa9e73..4712be3c 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/web/mod.rs @@ -2,6 +2,8 @@ mod interactive_auth; mod webview_host_validator; mod webview_options; -pub use interactive_auth::*; +#[allow(unused_imports)] pub use webview_host_validator::*; + +pub use interactive_auth::*; pub use webview_options::*; diff --git a/src/lib.rs b/src/lib.rs index 0f64c76d..ef703e7b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -276,6 +276,5 @@ pub(crate) mod api_default_imports { pub(crate) use graph_error::*; pub(crate) use graph_http::api_impl::*; - pub(crate) use crate::client::Graph; pub(crate) use crate::client::{map_errors, map_parameters, ResourceProvisioner}; } From 2baa4b623ceb42d6756f6a8fbf403e0c24af54f8 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 2 Jan 2024 01:54:23 -0500 Subject: [PATCH 075/118] Fix test-util feature --- graph-http/src/lib.rs | 3 +- .../auth_code_authorization_url.rs | 9 +-- graph-oauth/src/identity/token.rs | 2 +- src/client/graph.rs | 55 +++++++++++-------- src/lib.rs | 1 + tests/test-util-feature.rs | 11 ++-- 6 files changed, 46 insertions(+), 35 deletions(-) diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index e28e37c3..893fa949 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -18,7 +18,7 @@ pub mod traits; pub mod io_tools; pub(crate) mod internal { - pub use crate::blocking::*; + pub use crate::client::*; pub use crate::core::*; pub use crate::io_tools::*; @@ -27,7 +27,6 @@ pub(crate) mod internal { pub use crate::resource_identifier::*; pub use crate::traits::*; pub use crate::upload_session::*; - pub use crate::url::*; pub use graph_core::http::*; } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index bd281d06..290eada8 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -11,9 +11,9 @@ use graph_core::crypto::{secure_random_32, ProofKeyCodeExchange}; use graph_error::{IdentityResult, AF}; use crate::identity::{ - tracing_targets::INTERACTIVE_AUTH, AppConfig, AsQuery, - AuthorizationCodeAssertionCredentialBuilder, AuthorizationCodeCredentialBuilder, - AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, + AppConfig, AsQuery, AuthorizationCodeAssertionCredentialBuilder, + AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, + ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use crate::{Assertion, AuthorizationEvent, Secret}; @@ -24,7 +24,8 @@ use crate::identity::X509Certificate; #[cfg(feature = "interactive-auth")] use { crate::identity::{ - AuthorizationCodeCertificateCredentialBuilder, AuthorizationResponse, Token, + tracing_targets::INTERACTIVE_AUTH, AuthorizationCodeCertificateCredentialBuilder, + AuthorizationResponse, Token, }, crate::web::{ HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index c0637aa4..9a1427c2 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -122,7 +122,7 @@ pub struct Token { #[serde(flatten)] pub additional_fields: HashMap<String, Value>, #[serde(skip)] - log_pii: bool, + pub log_pii: bool, } impl Token { diff --git a/src/client/graph.rs b/src/client/graph.rs index d851bc93..27a80d32 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -238,8 +238,8 @@ impl GraphClient { /// .send() /// .await?; /// ``` - pub fn custom_endpoint(&mut self, custom_endpoint: &str) -> &mut GraphClient { - self.use_endpoint(custom_endpoint); + pub fn custom_endpoint(&mut self, url: &Url) -> &mut GraphClient { + self.use_endpoint(url); self } @@ -271,26 +271,29 @@ impl GraphClient { /// /// Example /// ```rust + /// use url::Url; /// use graph_rs_sdk::Graph; /// /// let mut client = Graph::new("ACCESS_TOKEN"); - /// client.use_endpoint("https://graph.microsoft.com/v1.0"); + /// client.use_endpoint(&Url::parse("https://graph.microsoft.com/v1.0").unwrap()); /// /// assert_eq!(client.url().to_string(), "https://graph.microsoft.com/v1.0".to_string()) /// ``` - pub fn use_endpoint(&mut self, custom_endpoint: &str) { - match self.allowed_host_validator.validate_str(custom_endpoint) { - HostIs::Valid => { - let url = Url::parse(custom_endpoint).expect("Unable to set custom endpoint"); + pub fn use_endpoint(&mut self, url: &Url) { + if cfg!(feature = "test-util") { + self.endpoint = url.clone(); + return; + } - if url.query().is_some() { - panic!( - "Invalid query - Provide only the scheme, host, and optional path of the Uri such as https://graph.microsoft.com/v1.0" - ); - } + if url.query().is_some() { + panic!( + "Invalid query - provide only the scheme, host, and optional path of the Uri such as https://graph.microsoft.com/v1.0" + ); + } - self.endpoint.set_host(url.host_str()).unwrap(); - self.endpoint.set_path(url.path()); + match self.allowed_host_validator.validate_url(url) { + HostIs::Valid => { + self.endpoint = url.clone(); } HostIs::Invalid => panic!("Invalid host"), } @@ -626,60 +629,64 @@ impl From<&PublicClientApplication<ResourceOwnerPasswordCredential>> for GraphCl mod test { use super::*; + fn test_url(url: &str) -> Url { + Url::parse(url).unwrap() + } + #[test] #[should_panic] fn try_invalid_host() { let mut client = GraphClient::new("token"); - client.custom_endpoint("https://example.org"); + client.custom_endpoint(&Url::parse("https://example.org").unwrap()); } #[test] #[should_panic] - fn try_invalid_scheme() { + fn try_invalid_http_scheme() { let mut client = GraphClient::new("token"); - client.custom_endpoint("http://example.org"); + client.custom_endpoint(&Url::parse("http://example.org").unwrap()); } #[test] #[should_panic] fn try_invalid_query() { let mut client = GraphClient::new("token"); - client.custom_endpoint("https://example.org?user=name"); + client.custom_endpoint(&Url::parse("https://example.org?user=name").unwrap()); } #[test] #[should_panic] fn try_invalid_path() { let mut client = GraphClient::new("token"); - client.custom_endpoint("https://example.org/v1"); + client.custom_endpoint(&Url::parse("https://example.org/v1").unwrap()); } #[test] #[should_panic] fn try_invalid_host2() { let mut client = GraphClient::new("token"); - client.use_endpoint("https://example.org"); + client.use_endpoint(&Url::parse("https://example.org").unwrap()); } #[test] #[should_panic] fn try_invalid_scheme2() { let mut client = GraphClient::new("token"); - client.use_endpoint("http://example.org"); + client.use_endpoint(&Url::parse("http://example.org").unwrap()); } #[test] #[should_panic] fn try_invalid_query2() { let mut client = GraphClient::new("token"); - client.use_endpoint("https://example.org?user=name"); + client.use_endpoint(&Url::parse("https://example.org?user=name").unwrap()); } #[test] #[should_panic] fn try_invalid_path2() { let mut client = GraphClient::new("token"); - client.use_endpoint("https://example.org/v1"); + client.use_endpoint(&Url::parse("https://example.org/v1").unwrap()); } #[test] @@ -696,7 +703,7 @@ mod test { let mut client = Graph::new("token"); for url in urls.iter() { - client.custom_endpoint(url); + client.custom_endpoint(&Url::parse(url).unwrap()); assert_eq!(client.url().clone(), Url::parse(url).unwrap()); } } diff --git a/src/lib.rs b/src/lib.rs index ef703e7b..06402b79 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,6 +204,7 @@ pub mod education; pub mod extended_properties; pub mod group_lifecycle_policies; pub mod groups; +/// The main identity APIs with starting path `identity/` pub mod identity_access; pub mod identity_governance; pub mod identity_providers; diff --git a/tests/test-util-feature.rs b/tests/test-util-feature.rs index 6bbcd3bd..74f5a4b8 100644 --- a/tests/test-util-feature.rs +++ b/tests/test-util-feature.rs @@ -1,5 +1,5 @@ -use graph_rs_sdk::{Graph, GraphClientConfiguration}; -use wiremock::matchers::{method, path}; +use graph_rs_sdk::{http::Url, Graph, GraphClientConfiguration, ODataQuery}; +use wiremock::matchers::{bearer_token, method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; /// Tests the test-util feature and setting https-only to false. @@ -9,6 +9,8 @@ async fn test_util_feature() { Mock::given(method("GET")) .and(path("/users")) + .and(query_param("$top", "10")) + .and(bearer_token("token")) .respond_with(ResponseTemplate::new(200)) .mount(&mock_server) .await; @@ -18,9 +20,10 @@ async fn test_util_feature() { .https_only(false); let mut client = Graph::from(graph_client_configuration); - client.use_endpoint(mock_server.uri().as_str()); + let uri = mock_server.uri(); + client.use_endpoint(&Url::parse(uri.as_str()).unwrap()); - let response = client.users().list_user().send().await.unwrap(); + let response = client.users().list_user().top("10").send().await.unwrap(); let status = response.status(); assert_eq!(status.as_u16(), 200); } From cc268467b064fb261862cbc80e3878828b9a8323 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 2 Jan 2024 02:10:57 -0500 Subject: [PATCH 076/118] Fix lints --- graph-http/src/lib.rs | 3 +- .../src/identity/application_options.rs | 10 ++---- .../auth_code_authorization_url.rs | 4 +-- .../credentials/device_code_credential.rs | 31 +++++++------------ graph-oauth/src/identity/credentials/mod.rs | 2 +- graph-oauth/src/lib.rs | 1 - src/client/graph.rs | 4 --- src/reports/mod.rs | 1 + 8 files changed, 21 insertions(+), 35 deletions(-) diff --git a/graph-http/src/lib.rs b/graph-http/src/lib.rs index 893fa949..0fff7a36 100644 --- a/graph-http/src/lib.rs +++ b/graph-http/src/lib.rs @@ -18,12 +18,13 @@ pub mod traits; pub mod io_tools; pub(crate) mod internal { - + pub use crate::client::*; pub use crate::core::*; pub use crate::io_tools::*; pub use crate::request_components::*; pub use crate::request_handler::*; + #[allow(unused_imports)] pub use crate::resource_identifier::*; pub use crate::traits::*; pub use crate::upload_session::*; diff --git a/graph-oauth/src/identity/application_options.rs b/graph-oauth/src/identity/application_options.rs index ee8e7c81..4cb8ae1f 100644 --- a/graph-oauth/src/identity/application_options.rs +++ b/graph-oauth/src/identity/application_options.rs @@ -52,17 +52,13 @@ impl ApplicationOptions { #[cfg(test)] mod test { - use std::fs::File; - use super::*; #[test] fn application_options_from_file() { - let file = File::open( - r#"../../../src/identity_/credentials/test/application_options/aad_options.json"#, - ) - .unwrap(); - let application_options: ApplicationOptions = serde_json::from_reader(file).unwrap(); + let file_content = include_str!("credentials/test/application_options/aad_options.json"); + let application_options: ApplicationOptions = serde_json::from_str(file_content).unwrap(); + assert_eq!( application_options.aad_authority_audience, Some(AadAuthorityAudience::PersonalMicrosoftAccount) diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 290eada8..7b417afa 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -16,7 +16,6 @@ use crate::identity::{ ResponseType, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; -use crate::{Assertion, AuthorizationEvent, Secret}; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; @@ -31,6 +30,7 @@ use { HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, WebViewOptions, WithInteractiveAuth, }, + crate::{Assertion, AuthorizationEvent, Secret}, graph_error::{AuthExecutionError, WebViewError, WebViewResult}, wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -353,10 +353,10 @@ impl AuthCodeAuthorizationUrlParameters { } } +#[cfg(feature = "interactive-auth")] mod internal { use super::*; - #[cfg(feature = "interactive-auth")] impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { fn webview( host_options: HostOptions, diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 6690414f..94964fa6 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -6,15 +6,15 @@ use std::str::FromStr; use std::time::Duration; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; +use graph_core::identity::ForceTokenRefresh; use http::{HeaderMap, HeaderName, HeaderValue}; use tracing::error; use url::Url; use uuid::Uuid; use crate::identity::{ - tracing_targets::INTERACTIVE_AUTH, AppConfig, Authority, AzureCloudInstance, - DeviceAuthorizationResponse, PollDeviceCodeEvent, PublicClientApplication, Token, - TokenCredentialExecutor, + AppConfig, Authority, AzureCloudInstance, DeviceAuthorizationResponse, PollDeviceCodeEvent, + PublicClientApplication, Token, TokenCredentialExecutor, }; use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; use graph_core::http::{ @@ -26,21 +26,14 @@ use graph_error::{ }; #[cfg(feature = "interactive-auth")] -use graph_error::WebViewDeviceCodeError; - -#[cfg(feature = "interactive-auth")] -use crate::web::WebViewOptions; - -#[cfg(feature = "interactive-auth")] -use crate::web::{HostOptions, InteractiveAuth}; - -#[cfg(feature = "interactive-auth")] -use crate::web::UserEvents; -use graph_core::identity::ForceTokenRefresh; -#[cfg(feature = "interactive-auth")] -use wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, +use { + crate::tracing_targets::INTERACTIVE_AUTH, + crate::web::{HostOptions, InteractiveAuth, UserEvents, WebViewOptions}, + graph_error::WebViewDeviceCodeError, + wry::{ + application::{event_loop::EventLoopProxy, window::Window}, + webview::{WebView, WebViewBuilder}, + }, }; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -577,10 +570,10 @@ impl DeviceCodePollingExecutor { } } +#[cfg(feature = "interactive-auth")] pub(crate) mod internal { use super::*; - #[cfg(feature = "interactive-auth")] impl InteractiveAuth for DeviceCodeCredential { fn webview( host_options: HostOptions, diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 27115b7e..9cfa1eb4 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -7,7 +7,7 @@ pub use authorization_code_certificate_credential::*; pub use authorization_code_credential::*; pub use bearer_token_credential::*; pub use client_assertion_credential::*; -pub use client_builder_impl::*; + pub use client_certificate_credential::*; pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index a1d516b7..f3e77654 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -55,7 +55,6 @@ pub mod web; pub(crate) mod internal { pub use crate::oauth_serializer::*; - pub use graph_core::http::*; } pub mod extensions { diff --git a/src/client/graph.rs b/src/client/graph.rs index 27a80d32..8511747f 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -629,10 +629,6 @@ impl From<&PublicClientApplication<ResourceOwnerPasswordCredential>> for GraphCl mod test { use super::*; - fn test_url(url: &str) -> Url { - Url::parse(url).unwrap() - } - #[test] #[should_panic] fn try_invalid_host() { diff --git a/src/reports/mod.rs b/src/reports/mod.rs index ccb6abe9..3394b383 100644 --- a/src/reports/mod.rs +++ b/src/reports/mod.rs @@ -1,5 +1,6 @@ mod manual_request; mod request; +#[allow(unused_imports)] pub use manual_request::*; pub use request::*; From 8f23c0d949bb4a93182dfff6d7776da44d773f40 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 4 Jan 2024 05:52:39 -0500 Subject: [PATCH 077/118] Fix test-util feature and update identity traits --- README.md | 8 +- .../auth_code_grant/interactive_auth.rs | 7 +- .../client_credentials_admin_consent.rs | 25 +- .../is_access_token_expired.rs | 15 - examples/identity_platform_auth/main.rs | 1 - examples/interactive_auth/auth_code.rs | 5 +- examples/interactive_auth/openid.rs | 8 +- examples/interactive_auth/webview_options.rs | 6 +- graph-codegen/src/api_types/request_client.rs | 4 +- .../src/api_types/request_metadata.rs | 2 +- graph-codegen/src/openapi/path_item.rs | 2 +- graph-codegen/src/parser/request.rs | 2 +- graph-oauth/Cargo.toml | 1 - .../src/identity/allowed_host_validator.rs | 8 +- .../identity/authorization_query_response.rs | 75 --- .../src/identity/credentials/app_config.rs | 7 +- .../auth_code_authorization_url.rs | 84 ++- ...authorization_code_assertion_credential.rs | 42 +- ...thorization_code_certificate_credential.rs | 42 +- .../authorization_code_credential.rs | 48 +- .../client_assertion_credential.rs | 18 +- .../client_certificate_credential.rs | 18 +- .../client_credentials_authorization_url.rs | 18 +- .../credentials/client_secret_credential.rs | 10 +- .../credentials/device_code_credential.rs | 37 +- .../credentials/legacy/implicit_credential.rs | 24 +- .../credentials/open_id_authorization_url.rs | 54 +- .../credentials/open_id_credential.rs | 38 +- .../resource_owner_password_credential.rs | 16 +- .../identity/credentials/x509_certificate.rs | 4 +- .../identity/device_authorization_response.rs | 2 +- .../src/identity/into_credential_builder.rs | 8 + graph-oauth/src/identity/mod.rs | 2 + graph-oauth/src/identity/token.rs | 2 +- .../{web => interactive}/interactive_auth.rs | 29 +- graph-oauth/src/{web => interactive}/mod.rs | 4 + .../webview_authorization_event.rs | 76 +++ .../webview_host_validator.rs | 2 +- .../{web => interactive}/webview_options.rs | 0 .../src/interactive/with_interactive_auth.rs | 13 + graph-oauth/src/lib.rs | 2 +- graph-oauth/src/oauth_serializer.rs | 547 +++++++----------- src/client/graph.rs | 72 ++- tests/test-util-feature.rs | 29 - tests/todo_tasks_request.rs | 1 - tests/upload_session_request.rs | 2 +- 46 files changed, 654 insertions(+), 766 deletions(-) delete mode 100644 examples/identity_platform_auth/is_access_token_expired.rs create mode 100644 graph-oauth/src/identity/into_credential_builder.rs rename graph-oauth/src/{web => interactive}/interactive_auth.rs (91%) rename graph-oauth/src/{web => interactive}/mod.rs (58%) create mode 100644 graph-oauth/src/interactive/webview_authorization_event.rs rename graph-oauth/src/{web => interactive}/webview_host_validator.rs (98%) rename graph-oauth/src/{web => interactive}/webview_options.rs (100%) create mode 100644 graph-oauth/src/interactive/with_interactive_auth.rs delete mode 100644 tests/test-util-feature.rs diff --git a/README.md b/README.md index 51b1d161..aa397669 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ fn main() -> GraphResult<()> { - `brotli`: Enables reqwest feature brotli. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. - `deflate`: Enables reqwest feature deflate. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. - `trust-dns`: Enables reqwest feature trust-dns. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -- `test-util`: Enables testing features. Currently only enables setting https-only to false for use in mocking frameworks. +- `test-util`: Enables testing features. Adds the ability to set a custom endpoint for mocking frameworks using the `use_test_endpoint` method on the `GraphClient`. + - Also allow http (disable https only) Default features: `default=["native-tls"]` @@ -997,10 +998,11 @@ handle the access token requests for you. Support for: +- OpenId, Auth Code Grant, Client Credentials, Device Code, Certificate Auth - Automatic Token Refresh -- Interactive Authentication +- Interactive Authentication | features = [`interactive-auth`] - Device Code Polling -- Authorization Using Certificates +- Authorization Using Certificates | features = [`openssl`] There are two main types for building your chosen OAuth or OpenId Connect Flow. diff --git a/examples/certificate_auth/auth_code_grant/interactive_auth.rs b/examples/certificate_auth/auth_code_grant/interactive_auth.rs index cd968052..61b57cdf 100644 --- a/examples/certificate_auth/auth_code_grant/interactive_auth.rs +++ b/examples/certificate_auth/auth_code_grant/interactive_auth.rs @@ -1,12 +1,13 @@ use graph_rs_sdk::identity::{ - web::WithInteractiveAuth, AuthorizationCodeCertificateCredential, - ConfidentialClientApplication, MapCredentialBuilder, PKey, X509Certificate, X509, + interactive::WithInteractiveAuth, ConfidentialClientApplication, IntoCredentialBuilder, PKey, + X509Certificate, X509, }; use graph_rs_sdk::GraphClient; use std::fs::File; use std::io::Read; use std::path::Path; use url::Url; + pub fn x509_certificate( client_id: &str, tenant: &str, @@ -43,7 +44,7 @@ fn interactive_auth( .with_scope(scope) .with_redirect_uri(redirect_uri) .with_interactive_auth(&x509certificate, Default::default()) - .map_to_credential_builder() + .into_credential_builder() .unwrap(); let confidential_client = credential_builder.build(); diff --git a/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs index 6c229869..9c5ebb7f 100644 --- a/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs +++ b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs @@ -21,7 +21,7 @@ // or admin. See examples/client_credentials.rs use graph_rs_sdk::error::IdentityResult; -use graph_rs_sdk::identity::ConfidentialClientApplication; +use graph_rs_sdk::identity::{ClientCredentialAdminConsentResponse, ConfidentialClientApplication}; use warp::Filter; // The client_id must be changed before running this example. @@ -54,24 +54,21 @@ fn get_admin_consent_url() -> IdentityResult<url::Url> { // After admin consent has been granted see examples/client_credential.rs for how to // programmatically get access tokens using the client credentials flow. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct ClientCredentialsResponse { - admin_consent: bool, - tenant: String, -} - async fn handle_redirect( - client_credential_option: Option<ClientCredentialsResponse>, + client_credential_option: Option<ClientCredentialAdminConsentResponse>, ) -> Result<Box<dyn warp::Reply>, warp::Rejection> { match client_credential_option { Some(client_credential_response) => { // Print out for debugging purposes. println!("{client_credential_response:#?}"); - // Generic login page response. - Ok(Box::new( - "Successfully Logged In! You can close your browser.", - )) + // Generic response page. + if client_credential_response.admin_consent { + Ok(Box::new("Admin consent granted")) + } else { + // Generic login page response. + Ok(Box::new("Failed to grant consent")) + } } None => Err(warp::reject()), } @@ -87,10 +84,10 @@ async fn handle_redirect( /// } /// ``` pub async fn start_server_main() { - let query = warp::query::<ClientCredentialsResponse>() + let query = warp::query::<ClientCredentialAdminConsentResponse>() .map(Some) .or_else(|_| async { - Ok::<(Option<ClientCredentialsResponse>,), std::convert::Infallible>((None,)) + Ok::<(Option<ClientCredentialAdminConsentResponse>,), std::convert::Infallible>((None,)) }); let routes = warp::get() diff --git a/examples/identity_platform_auth/is_access_token_expired.rs b/examples/identity_platform_auth/is_access_token_expired.rs deleted file mode 100644 index 0397d65a..00000000 --- a/examples/identity_platform_auth/is_access_token_expired.rs +++ /dev/null @@ -1,15 +0,0 @@ -use graph_rs_sdk::identity::Token; -use std::thread; -use std::time::Duration; - -pub fn is_access_token_expired() { - let mut token = Token::default(); - token.with_expires_in(1); - thread::sleep(Duration::from_secs(3)); - assert!(token.is_expired()); - - let mut token = Token::default(); - token.with_expires_in(10); - thread::sleep(Duration::from_secs(4)); - assert!(!token.is_expired()); -} diff --git a/examples/identity_platform_auth/main.rs b/examples/identity_platform_auth/main.rs index 9a0946df..404db6b5 100644 --- a/examples/identity_platform_auth/main.rs +++ b/examples/identity_platform_auth/main.rs @@ -22,7 +22,6 @@ mod client_credentials; mod device_code; mod environment_credential; mod getting_tokens_manually; -mod is_access_token_expired; mod openid; use graph_rs_sdk::identity::{ diff --git a/examples/interactive_auth/auth_code.rs b/examples/interactive_auth/auth_code.rs index 24c43cc7..5a80618b 100644 --- a/examples/interactive_auth/auth_code.rs +++ b/examples/interactive_auth/auth_code.rs @@ -1,6 +1,7 @@ use graph_rs_sdk::{ identity::{ - web::WithInteractiveAuth, AuthorizationCodeCredential, MapCredentialBuilder, Secret, + interactive::WithInteractiveAuth, AuthorizationCodeCredential, IntoCredentialBuilder, + Secret, }, GraphClient, }; @@ -41,7 +42,7 @@ async fn authenticate( .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(Url::parse(redirect_uri)?) .with_interactive_auth(Secret("secret".to_string()), Default::default()) - .map_to_credential_builder()?; + .into_credential_builder()?; debug!("{authorization_query_response:#?}"); diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index cdf9056e..430a98a0 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -1,8 +1,8 @@ use graph_rs_sdk::{ - identity::{OpenIdCredential, ResponseMode, ResponseType}, + http::Url, + identity::{IntoCredentialBuilder, OpenIdCredential, ResponseMode, ResponseType}, GraphClient, }; -use url::Url; async fn openid_authenticate( tenant_id: &str, @@ -20,8 +20,8 @@ async fn openid_authenticate( .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_redirect_uri(Url::parse(redirect_uri)?) - .with_interactive_auth(client_secret, Default::default())? - .into_result()?; + .with_interactive_auth(client_secret, Default::default()) + .into_credential_builder()?; debug!("{authorization_query_response:#?}"); diff --git a/examples/interactive_auth/webview_options.rs b/examples/interactive_auth/webview_options.rs index 5655b292..dd9b1668 100644 --- a/examples/interactive_auth/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -1,6 +1,6 @@ use graph_rs_sdk::identity::{ - web::Theme, web::WebViewOptions, web::WithInteractiveAuth, AuthorizationCodeCredential, - MapCredentialBuilder, Secret, + interactive::Theme, interactive::WebViewOptions, interactive::WithInteractiveAuth, + AuthorizationCodeCredential, IntoCredentialBuilder, Secret, }; use graph_rs_sdk::GraphClient; use std::collections::HashSet; @@ -64,7 +64,7 @@ async fn customize_webview( .with_scope(scope) .with_redirect_uri(Url::parse(redirect_uri)?) .with_interactive_auth(Secret(client_secret.to_string()), get_webview_options()) - .map_to_credential_builder()?; + .into_credential_builder()?; let confidential_client = credential_builder.build(); diff --git a/graph-codegen/src/api_types/request_client.rs b/graph-codegen/src/api_types/request_client.rs index eda3cb15..4b8b8098 100644 --- a/graph-codegen/src/api_types/request_client.rs +++ b/graph-codegen/src/api_types/request_client.rs @@ -22,7 +22,7 @@ impl RequestClientList { pub fn client_links(&self) -> BTreeMap<String, Vec<String>> { let mut links_map: BTreeMap<String, Vec<String>> = BTreeMap::new(); for (_name, metadata) in self.clients.iter() { - if let Some(m) = metadata.get(0) { + if let Some(m) = metadata.front() { let links = m.operation_mapping.struct_links(); links_map.extend(links); } @@ -50,7 +50,7 @@ impl From<VecDeque<RequestMetadata>> for RequestClientList { let mut links_map: BTreeMap<String, Vec<String>> = BTreeMap::new(); for (_name, metadata) in clients.iter() { - if let Some(m) = metadata.get(0) { + if let Some(m) = metadata.front() { let links = m.operation_mapping.struct_links(); links_map.extend(links); } diff --git a/graph-codegen/src/api_types/request_metadata.rs b/graph-codegen/src/api_types/request_metadata.rs index 99109a51..d61f6ce3 100644 --- a/graph-codegen/src/api_types/request_metadata.rs +++ b/graph-codegen/src/api_types/request_metadata.rs @@ -446,7 +446,7 @@ impl MacroQueueWriter for PathMetadata { fn parent(&self) -> String { self.metadata - .get(0) + .front() .map(|m| m.parent.clone()) .unwrap_or_default() } diff --git a/graph-codegen/src/openapi/path_item.rs b/graph-codegen/src/openapi/path_item.rs index 2423b57d..1aabc040 100644 --- a/graph-codegen/src/openapi/path_item.rs +++ b/graph-codegen/src/openapi/path_item.rs @@ -136,7 +136,7 @@ impl PathItem { } pub fn operations(&self) -> VecDeque<Operation> { - vec![ + [ self.get.as_ref(), self.put.as_ref(), self.post.as_ref(), diff --git a/graph-codegen/src/parser/request.rs b/graph-codegen/src/parser/request.rs index 5561cf6d..b935abf1 100644 --- a/graph-codegen/src/parser/request.rs +++ b/graph-codegen/src/parser/request.rs @@ -403,7 +403,7 @@ impl RequestSet { pub fn group_by_operation_mapping(&self) -> HashMap<String, Vec<RequestMap>> { let mut map: HashMap<String, Vec<RequestMap>> = HashMap::new(); for request_map in self.set.iter() { - if let Some(request) = request_map.requests.get(0) { + if let Some(request) = request_map.requests.front() { let operation_mapping = request.operation_mapping.to_string(); map.entry_modify_insert(operation_mapping, request_map.clone()); } diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index a786173b..6050d436 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -19,7 +19,6 @@ exclude = [ anyhow = { version = "1.0.69", features = ["backtrace"]} async-trait = "0.1.35" base64 = "0.21.0" -either = "1.9.0" dyn-clone = "1.0.14" hex = "0.4.3" http = "0.2.11" diff --git a/graph-oauth/src/identity/allowed_host_validator.rs b/graph-oauth/src/identity/allowed_host_validator.rs index 6e49061c..395d8a17 100644 --- a/graph-oauth/src/identity/allowed_host_validator.rs +++ b/graph-oauth/src/identity/allowed_host_validator.rs @@ -114,7 +114,7 @@ impl ValidateHosts for AllowedHostValidator { impl Default for AllowedHostValidator { fn default() -> Self { - let urls: HashSet<Url> = vec![ + let urls: HashSet<Url> = [ "https://graph.microsoft.com", "https://graph.microsoft.us", "https://dod-graph.microsoft.us", @@ -137,7 +137,7 @@ mod test { #[test] fn test_valid_hosts() { - let valid_hosts: Vec<String> = vec![ + let valid_hosts: Vec<String> = [ "graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", @@ -171,7 +171,7 @@ mod test { "example.org", ]; - let valid_hosts: Vec<Url> = vec![ + let valid_hosts: Vec<Url> = [ "graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", @@ -199,7 +199,7 @@ mod test { #[test] fn test_allowed_host_validator() { - let valid_hosts: Vec<String> = vec![ + let valid_hosts: Vec<String> = [ "graph.microsoft.com", "graph.microsoft.us", "dod-graph.microsoft.us", diff --git a/graph-oauth/src/identity/authorization_query_response.rs b/graph-oauth/src/identity/authorization_query_response.rs index b8469419..567e3246 100644 --- a/graph-oauth/src/identity/authorization_query_response.rs +++ b/graph-oauth/src/identity/authorization_query_response.rs @@ -1,4 +1,3 @@ -use graph_error::{WebViewError, WebViewResult}; use serde::Deserializer; use serde_json::Value; use std::collections::HashMap; @@ -187,80 +186,6 @@ impl Debug for AuthorizationResponse { } } -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum AuthorizationImpeded { - WindowClosed(String), - InvalidUri(String), -} - -#[derive(Clone, Debug)] -pub enum AuthorizationEvent<CredentialBuilder: Clone + Debug> { - Authorized { - authorization_response: AuthorizationResponse, - credential_builder: CredentialBuilder, - }, - Unauthorized(AuthorizationResponse), - WindowClosed(String), -} - -impl<CredentialBuilder: Clone + Debug> AuthorizationEvent<CredentialBuilder> { - pub fn into_result(self) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { - match self { - AuthorizationEvent::Authorized { - authorization_response, - credential_builder, - } => Ok((authorization_response, credential_builder)), - AuthorizationEvent::Unauthorized(authorization_response) => { - Err(WebViewError::Authorization { - error: authorization_response - .error - .map(|query_error| query_error.to_string()) - .unwrap_or_default(), - error_description: authorization_response.error_description.unwrap_or_default(), - error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), - }) - } - AuthorizationEvent::WindowClosed(reason) => Err(WebViewError::WindowClosed(reason)), - } - } -} - -pub trait MapCredentialBuilder<CredentialBuilder: Clone + Debug> { - fn map_to_credential_builder(self) - -> WebViewResult<(AuthorizationResponse, CredentialBuilder)>; -} - -impl<CredentialBuilder: Clone + Debug> MapCredentialBuilder<CredentialBuilder> - for WebViewResult<AuthorizationEvent<CredentialBuilder>> -{ - fn map_to_credential_builder( - self, - ) -> WebViewResult<(AuthorizationResponse, CredentialBuilder)> { - match self { - Ok(auth_event) => match auth_event { - AuthorizationEvent::Authorized { - authorization_response, - credential_builder, - } => Ok((authorization_response, credential_builder)), - AuthorizationEvent::Unauthorized(authorization_response) => { - Err(WebViewError::Authorization { - error: authorization_response - .error - .map(|query_error| query_error.to_string()) - .unwrap_or_default(), - error_description: authorization_response - .error_description - .unwrap_or_default(), - error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), - }) - } - AuthorizationEvent::WindowClosed(reason) => Err(WebViewError::WindowClosed(reason)), - }, - Err(err) => Err(err), - } - } -} - #[cfg(test)] mod test { use super::*; diff --git a/graph-oauth/src/identity/credentials/app_config.rs b/graph-oauth/src/identity/credentials/app_config.rs index 160f17fc..2a9c3330 100644 --- a/graph-oauth/src/identity/credentials/app_config.rs +++ b/graph-oauth/src/identity/credentials/app_config.rs @@ -138,11 +138,8 @@ impl Debug for AppConfig { impl AppConfig { fn generate_cache_id(client_id: Uuid, tenant_id: Option<&String>) -> String { if let Some(tenant_id) = tenant_id.as_ref() { - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(format!( - "{},{}", - tenant_id, - client_id.to_string() - )) + base64::engine::general_purpose::URL_SAFE_NO_PAD + .encode(format!("{},{}", tenant_id, client_id)) } else { base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(client_id.to_string()) } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 7b417afa..8c31f2e2 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -15,7 +15,7 @@ use crate::identity::{ AuthorizationCodeCredentialBuilder, AuthorizationUrl, AzureCloudInstance, Prompt, ResponseMode, ResponseType, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; #[cfg(feature = "openssl")] use crate::identity::X509Certificate; @@ -26,11 +26,11 @@ use { tracing_targets::INTERACTIVE_AUTH, AuthorizationCodeCertificateCredentialBuilder, AuthorizationResponse, Token, }, - crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, - WebViewOptions, WithInteractiveAuth, + crate::interactive::{ + HostOptions, InteractiveAuthEvent, UserEvents, WebViewAuth, WebViewAuthorizationEvent, + WebViewHostValidator, WebViewOptions, WithInteractiveAuth, }, - crate::{Assertion, AuthorizationEvent, Secret}, + crate::{Assertion, Secret}, graph_error::{AuthExecutionError, WebViewError, WebViewResult}, wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -239,13 +239,8 @@ impl AuthCodeAuthorizationUrlParameters { let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - AuthCodeAuthorizationUrlParameters::interactive_auth( - uri, - vec![redirect_uri], - options, - sender, - ) - .unwrap(); + AuthCodeAuthorizationUrlParameters::run(uri, vec![redirect_uri], options, sender) + .unwrap(); }); let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -266,7 +261,7 @@ impl AuthCodeAuthorizationUrlParameters { .or(uri.fragment()) .ok_or(WebViewError::InvalidUri(format!( "uri missing query or fragment: {}", - uri.to_string() + uri )))?; let response_query: AuthorizationResponse = @@ -309,13 +304,8 @@ impl AuthCodeAuthorizationUrlParameters { let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - AuthCodeAuthorizationUrlParameters::interactive_auth( - uri, - vec![redirect_uri], - options, - sender, - ) - .unwrap(); + AuthCodeAuthorizationUrlParameters::run(uri, vec![redirect_uri], options, sender) + .unwrap(); }); let mut iter = receiver.try_iter(); let mut next = iter.next(); @@ -336,7 +326,7 @@ impl AuthCodeAuthorizationUrlParameters { .or(uri.fragment()) .ok_or(WebViewError::InvalidUri(format!( "uri missing query or fragment: {}", - uri.to_string() + uri )))?; let response_query: AuthorizationResponse = @@ -357,7 +347,7 @@ impl AuthCodeAuthorizationUrlParameters { mod internal { use super::*; - impl InteractiveAuth for AuthCodeAuthorizationUrlParameters { + impl WebViewAuth for AuthCodeAuthorizationUrlParameters { fn webview( host_options: HostOptions, window: Window, @@ -407,7 +397,7 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { &self, azure_cloud_instance: &AzureCloudInstance, ) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { if redirect_uri.as_str().trim().is_empty() { @@ -491,20 +481,20 @@ impl AuthorizationUrl for AuthCodeAuthorizationUrlParameters { let query = serializer.encode_query( vec![ - OAuthParameter::ResponseMode, - OAuthParameter::State, - OAuthParameter::Prompt, - OAuthParameter::LoginHint, - OAuthParameter::DomainHint, - OAuthParameter::Nonce, - OAuthParameter::CodeChallenge, - OAuthParameter::CodeChallengeMethod, + AuthParameter::ResponseMode, + AuthParameter::State, + AuthParameter::Prompt, + AuthParameter::LoginHint, + AuthParameter::DomainHint, + AuthParameter::Nonce, + AuthParameter::CodeChallenge, + AuthParameter::CodeChallengeMethod, ], vec![ - OAuthParameter::ClientId, - OAuthParameter::ResponseType, - OAuthParameter::RedirectUri, - OAuthParameter::Scope, + AuthParameter::ClientId, + AuthParameter::ResponseType, + AuthParameter::RedirectUri, + AuthParameter::Scope, ], )?; @@ -723,14 +713,16 @@ impl WithInteractiveAuth<Secret> for AuthCodeAuthorizationUrlParameterBuilder { &self, auth_type: Secret, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { + ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; if authorization_response.is_err() { tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); - return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + return Ok(WebViewAuthorizationEvent::Unauthorized( + authorization_response, + )); } tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); @@ -750,7 +742,7 @@ impl WithInteractiveAuth<Secret> for AuthCodeAuthorizationUrlParameterBuilder { }; credential_builder.with_client_secret(auth_type.0); - Ok(AuthorizationEvent::Authorized { + Ok(WebViewAuthorizationEvent::Authorized { authorization_response, credential_builder, }) @@ -765,14 +757,16 @@ impl WithInteractiveAuth<Assertion> for AuthCodeAuthorizationUrlParameterBuilder &self, auth_type: Assertion, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { + ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; if authorization_response.is_err() { tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); - return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + return Ok(WebViewAuthorizationEvent::Unauthorized( + authorization_response, + )); } tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); @@ -791,7 +785,7 @@ impl WithInteractiveAuth<Assertion> for AuthCodeAuthorizationUrlParameterBuilder }; credential_builder.with_client_assertion(auth_type.0); - Ok(AuthorizationEvent::Authorized { + Ok(WebViewAuthorizationEvent::Authorized { authorization_response, credential_builder, }) @@ -807,14 +801,16 @@ impl WithInteractiveAuth<&X509Certificate> for AuthCodeAuthorizationUrlParameter &self, auth_type: &X509Certificate, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>> { + ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>> { let authorization_response = self .credential .interactive_webview_authentication(options)?; if authorization_response.is_err() { tracing::debug!(target: INTERACTIVE_AUTH, "error in authorization query or fragment from redirect uri"); - return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + return Ok(WebViewAuthorizationEvent::Unauthorized( + authorization_response, + )); } tracing::debug!(target: INTERACTIVE_AUTH, "parsed authorization query or fragment from redirect uri"); @@ -835,7 +831,7 @@ impl WithInteractiveAuth<&X509Certificate> for AuthCodeAuthorizationUrlParameter }; credential_builder.with_x509(auth_type)?; - Ok(AuthorizationEvent::Authorized { + Ok(WebViewAuthorizationEvent::Authorized { authorization_response, credential_builder, }) diff --git a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs index 84052b9a..a372b8f5 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_assertion_credential.rs @@ -18,7 +18,7 @@ use crate::identity::{ AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; credential_builder!( AuthorizationCodeAssertionCredentialBuilder, @@ -237,14 +237,14 @@ impl TokenCache for AuthorizationCodeAssertionCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AF::result(OAuthParameter::ClientId); + return AF::result(AuthParameter::ClientId); } if self.client_assertion.trim().is_empty() { - return AF::result(OAuthParameter::ClientAssertion); + return AF::result(AuthParameter::ClientAssertion); } if self.client_assertion_type.trim().is_empty() { @@ -268,7 +268,7 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AF::msg_result( - OAuthParameter::RefreshToken.alias(), + AuthParameter::RefreshToken.alias(), "refresh_token is empty - cannot be an empty string", ); } @@ -278,19 +278,19 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { .grant_type("refresh_token"); return serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::RefreshToken, - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::RefreshToken, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AF::msg_result( - OAuthParameter::AuthorizationCode.alias(), + AuthParameter::AuthorizationCode.alias(), "authorization_code is empty - cannot be an empty string", ); } @@ -300,14 +300,14 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { .grant_type("authorization_code"); return serializer.as_credential_map( - vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![AuthParameter::Scope, AuthParameter::CodeVerifier], vec![ - OAuthParameter::AuthorizationCode, - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::RedirectUri, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::AuthorizationCode, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::RedirectUri, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ); } @@ -315,8 +315,8 @@ impl TokenCredentialExecutor for AuthorizationCodeAssertionCredential { AF::msg_result( format!( "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() + AuthParameter::AuthorizationCode.alias(), + AuthParameter::RefreshToken.alias() ), "Either authorization code or refresh token is required", ) diff --git a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs index e1e49707..d1d4d019 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_certificate_credential.rs @@ -20,7 +20,7 @@ use crate::identity::{ AppConfig, AuthCodeAuthorizationUrlParameterBuilder, Authority, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, CLIENT_ASSERTION_TYPE, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; credential_builder!( AuthorizationCodeCertificateCredentialBuilder, @@ -285,14 +285,14 @@ impl TokenCache for AuthorizationCodeCertificateCredential { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AF::result(OAuthParameter::ClientId); + return AF::result(AuthParameter::ClientId); } if self.client_assertion.trim().is_empty() { - return AF::result(OAuthParameter::ClientAssertion); + return AF::result(AuthParameter::ClientAssertion); } if self.client_assertion_type.trim().is_empty() { @@ -316,7 +316,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AF::msg_result( - OAuthParameter::RefreshToken.alias(), + AuthParameter::RefreshToken.alias(), "refresh_token is empty - cannot be an empty string", ); } @@ -326,19 +326,19 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { .grant_type("refresh_token"); return serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::RefreshToken, - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::RefreshToken, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AF::msg_result( - OAuthParameter::AuthorizationCode.alias(), + AuthParameter::AuthorizationCode.alias(), "authorization_code is empty - cannot be an empty string", ); } @@ -348,14 +348,14 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { .grant_type("authorization_code"); return serializer.as_credential_map( - vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![AuthParameter::Scope, AuthParameter::CodeVerifier], vec![ - OAuthParameter::AuthorizationCode, - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::RedirectUri, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::AuthorizationCode, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::RedirectUri, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ); } @@ -363,8 +363,8 @@ impl TokenCredentialExecutor for AuthorizationCodeCertificateCredential { AF::msg_result( format!( "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() + AuthParameter::AuthorizationCode.alias(), + AuthParameter::RefreshToken.alias() ), "Either authorization code or refresh token is required", ) diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 46755151..5cd1d3d5 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -18,7 +18,7 @@ use crate::identity::{ tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use crate::AuthCodeAuthorizationUrlParameterBuilder; credential_builder!( @@ -384,14 +384,14 @@ impl From<AuthorizationCodeCredential> for AuthorizationCodeCredentialBuilder { #[async_trait] impl TokenCredentialExecutor for AuthorizationCodeCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AF::result(OAuthParameter::ClientId.alias()); + return AF::result(AuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { - return AF::result(OAuthParameter::ClientSecret.alias()); + return AF::result(AuthParameter::ClientSecret.alias()); } serializer @@ -407,12 +407,12 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { .refresh_token(refresh_token.as_ref()); return serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RefreshToken, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::ClientSecret, + AuthParameter::RefreshToken, + AuthParameter::GrantType, ], ); } @@ -425,7 +425,7 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { if should_attempt_refresh { let refresh_token = self.refresh_token.clone().unwrap_or_default(); if refresh_token.trim().is_empty() { - return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); + return AF::msg_result(AuthParameter::RefreshToken, "Refresh token is empty"); } serializer @@ -433,18 +433,18 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { .refresh_token(refresh_token.as_ref()); return serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RefreshToken, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::ClientSecret, + AuthParameter::RefreshToken, + AuthParameter::GrantType, ], ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AF::msg_result( - OAuthParameter::AuthorizationCode.alias(), + AuthParameter::AuthorizationCode.alias(), "Authorization code is empty", ); } @@ -462,13 +462,13 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { } return serializer.as_credential_map( - vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![AuthParameter::Scope, AuthParameter::CodeVerifier], vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::AuthorizationCode, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::ClientSecret, + AuthParameter::RedirectUri, + AuthParameter::AuthorizationCode, + AuthParameter::GrantType, ], ); } @@ -476,8 +476,8 @@ impl TokenCredentialExecutor for AuthorizationCodeCredential { AF::msg_result( format!( "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() + AuthParameter::AuthorizationCode.alias(), + AuthParameter::RefreshToken.alias() ), "Either authorization code or refresh token is required", ) diff --git a/graph-oauth/src/identity/credentials/client_assertion_credential.rs b/graph-oauth/src/identity/credentials/client_assertion_credential.rs index 159e8256..c989d95f 100644 --- a/graph-oauth/src/identity/credentials/client_assertion_credential.rs +++ b/graph-oauth/src/identity/credentials/client_assertion_credential.rs @@ -6,7 +6,7 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use uuid::Uuid; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; use graph_core::identity::ForceTokenRefresh; @@ -156,14 +156,14 @@ impl TokenCache for ClientAssertionCredential { #[async_trait] impl TokenCredentialExecutor for ClientAssertionCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.client_id().to_string(); if client_id.trim().is_empty() { - return AF::result(OAuthParameter::ClientId.alias()); + return AF::result(AuthParameter::ClientId.alias()); } if self.client_assertion.trim().is_empty() { - return AF::result(OAuthParameter::ClientAssertion.alias()); + return AF::result(AuthParameter::ClientAssertion.alias()); } if self.client_assertion_type.trim().is_empty() { @@ -178,12 +178,12 @@ impl TokenCredentialExecutor for ClientAssertionCredential { .grant_type("client_credentials"); serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ) } diff --git a/graph-oauth/src/identity/credentials/client_certificate_credential.rs b/graph-oauth/src/identity/credentials/client_certificate_credential.rs index da08e70d..bbf20f1f 100644 --- a/graph-oauth/src/identity/credentials/client_certificate_credential.rs +++ b/graph-oauth/src/identity/credentials/client_certificate_credential.rs @@ -19,7 +19,7 @@ use crate::identity::{ ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; pub(crate) static CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; @@ -169,14 +169,14 @@ impl TokenCache for ClientCertificateCredential { #[async_trait] impl TokenCredentialExecutor for ClientCertificateCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(AuthParameter::ClientId.alias()); } if self.client_assertion.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::ClientAssertion.alias()); + return AuthorizationFailure::result(AuthParameter::ClientAssertion.alias()); } if self.client_assertion_type.trim().is_empty() { @@ -191,12 +191,12 @@ impl TokenCredentialExecutor for ClientCertificateCredential { .set_scope(self.app_config.scope.clone()); serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::ClientId, - OAuthParameter::GrantType, - OAuthParameter::ClientAssertion, - OAuthParameter::ClientAssertionType, + AuthParameter::ClientId, + AuthParameter::GrantType, + AuthParameter::ClientAssertion, + AuthParameter::ClientAssertionType, ], ) } diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index a59dd857..c5e48580 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -6,12 +6,18 @@ use uuid::Uuid; use graph_error::{AuthorizationFailure, IdentityResult}; use crate::identity::{credentials::app_config::AppConfig, Authority, AzureCloudInstance}; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use crate::{ClientAssertionCredentialBuilder, ClientSecretCredentialBuilder}; #[cfg(feature = "openssl")] use crate::identity::{ClientCertificateCredentialBuilder, X509Certificate}; +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct ClientCredentialAdminConsentResponse { + pub admin_consent: bool, + pub tenant: String, +} + #[derive(Clone)] pub struct ClientCredentialsAuthorizationUrlParameters { /// The client (application) ID of the service principal @@ -72,14 +78,14 @@ impl ClientCredentialsAuthorizationUrlParameters { } pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.trim().is_empty() || self.app_config.client_id.is_nil() { - return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(AuthParameter::ClientId.alias()); } if self.app_config.redirect_uri.is_none() { - return AuthorizationFailure::result(OAuthParameter::RedirectUri.alias()); + return AuthorizationFailure::result(AuthParameter::RedirectUri.alias()); } if let Some(redirect_uri) = self.app_config.redirect_uri.as_ref() { @@ -94,8 +100,8 @@ impl ClientCredentialsAuthorizationUrlParameters { let mut uri = azure_cloud_instance.admin_consent_uri(&self.app_config.authority)?; let query = serializer.encode_query( - vec![OAuthParameter::State], - vec![OAuthParameter::ClientId, OAuthParameter::RedirectUri], + vec![AuthParameter::State], + vec![AuthParameter::ClientId, AuthParameter::RedirectUri], )?; uri.set_query(Some(query.as_str())); Ok(uri) diff --git a/graph-oauth/src/identity/credentials/client_secret_credential.rs b/graph-oauth/src/identity/credentials/client_secret_credential.rs index 8fc0a9d0..8cf0980d 100644 --- a/graph-oauth/src/identity/credentials/client_secret_credential.rs +++ b/graph-oauth/src/identity/credentials/client_secret_credential.rs @@ -16,7 +16,7 @@ use crate::identity::{ AzureCloudInstance, ClientCredentialsAuthorizationUrlParameterBuilder, ConfidentialClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; credential_builder!( ClientSecretCredentialBuilder, @@ -168,14 +168,14 @@ impl TokenCache for ClientSecretCredential { #[async_trait] impl TokenCredentialExecutor for ClientSecretCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AuthorizationFailure::result(OAuthParameter::ClientId); + return AuthorizationFailure::result(AuthParameter::ClientId); } if self.client_secret.trim().is_empty() { - return AuthorizationFailure::result(OAuthParameter::ClientSecret); + return AuthorizationFailure::result(AuthParameter::ClientSecret); } serializer @@ -186,7 +186,7 @@ impl TokenCredentialExecutor for ClientSecretCredential { // Don't include ClientId and Client Secret in the fields for form url encode because // Client Id and Client Secret are already included as basic auth. - serializer.as_credential_map(vec![OAuthParameter::Scope], vec![OAuthParameter::GrantType]) + serializer.as_credential_map(vec![AuthParameter::Scope], vec![AuthParameter::GrantType]) } fn client_id(&self) -> &Uuid { diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 94964fa6..76becef0 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -16,7 +16,7 @@ use crate::identity::{ AppConfig, Authority, AzureCloudInstance, DeviceAuthorizationResponse, PollDeviceCodeEvent, PublicClientApplication, Token, TokenCredentialExecutor, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use graph_core::http::{ AsyncResponseConverterExt, HttpResponseExt, JsonHttpResponse, ResponseConverterExt, }; @@ -27,8 +27,8 @@ use graph_error::{ #[cfg(feature = "interactive-auth")] use { + crate::interactive::{HostOptions, UserEvents, WebViewAuth, WebViewOptions}, crate::tracing_targets::INTERACTIVE_AUTH, - crate::web::{HostOptions, InteractiveAuth, UserEvents, WebViewOptions}, graph_error::WebViewDeviceCodeError, wry::{ application::{event_loop::EventLoopProxy, window::Window}, @@ -241,10 +241,10 @@ impl TokenCredentialExecutor for DeviceCodeCredential { } fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AuthorizationFailure::result(OAuthParameter::ClientId.alias()); + return AuthorizationFailure::result(AuthParameter::ClientId.alias()); } serializer @@ -254,7 +254,7 @@ impl TokenCredentialExecutor for DeviceCodeCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { return AuthorizationFailure::msg_result( - OAuthParameter::RefreshToken.alias(), + AuthParameter::RefreshToken.alias(), "Found empty string for refresh token", ); } @@ -266,16 +266,16 @@ impl TokenCredentialExecutor for DeviceCodeCredential { return serializer.as_credential_map( vec![], vec![ - OAuthParameter::ClientId, - OAuthParameter::RefreshToken, - OAuthParameter::Scope, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::RefreshToken, + AuthParameter::Scope, + AuthParameter::GrantType, ], ); } else if let Some(device_code) = self.device_code.as_ref() { if device_code.trim().is_empty() { return AuthorizationFailure::msg_result( - OAuthParameter::DeviceCode.alias(), + AuthParameter::DeviceCode.alias(), "Found empty string for device code", ); } @@ -287,18 +287,15 @@ impl TokenCredentialExecutor for DeviceCodeCredential { return serializer.as_credential_map( vec![], vec![ - OAuthParameter::ClientId, - OAuthParameter::DeviceCode, - OAuthParameter::Scope, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::DeviceCode, + AuthParameter::Scope, + AuthParameter::GrantType, ], ); } - serializer.as_credential_map( - vec![], - vec![OAuthParameter::ClientId, OAuthParameter::Scope], - ) + serializer.as_credential_map(vec![], vec![AuthParameter::ClientId, AuthParameter::Scope]) } fn client_id(&self) -> &Uuid { @@ -574,7 +571,7 @@ impl DeviceCodePollingExecutor { pub(crate) mod internal { use super::*; - impl InteractiveAuth for DeviceCodeCredential { + impl WebViewAuth for DeviceCodeCredential { fn webview( host_options: HostOptions, window: Window, @@ -633,7 +630,7 @@ impl DeviceCodeInteractiveAuth { let (sender, _receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - DeviceCodeCredential::interactive_auth(url, vec![], options, sender).unwrap(); + DeviceCodeCredential::run(url, vec![], options, sender).unwrap(); }); self.poll() diff --git a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs index 5e0a6387..4e55a20a 100644 --- a/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs +++ b/graph-oauth/src/identity/credentials/legacy/implicit_credential.rs @@ -1,6 +1,6 @@ use crate::identity::credentials::app_config::AppConfig; use crate::identity::{AzureCloudInstance, Prompt, ResponseMode, ResponseType}; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use graph_core::crypto::secure_random_32; use graph_error::{AuthorizationFailure, IdentityResult, AF}; use http::{HeaderMap, HeaderName, HeaderValue}; @@ -108,7 +108,7 @@ impl ImplicitCredential { } pub fn url_with_host(&self, azure_cloud_instance: &AzureCloudInstance) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { return AuthorizationFailure::result("client_id"); @@ -178,18 +178,18 @@ impl ImplicitCredential { let query = serializer.encode_query( vec![ - OAuthParameter::RedirectUri, - OAuthParameter::ResponseMode, - OAuthParameter::State, - OAuthParameter::Prompt, - OAuthParameter::LoginHint, - OAuthParameter::DomainHint, + AuthParameter::RedirectUri, + AuthParameter::ResponseMode, + AuthParameter::State, + AuthParameter::Prompt, + AuthParameter::LoginHint, + AuthParameter::DomainHint, ], vec![ - OAuthParameter::ClientId, - OAuthParameter::ResponseType, - OAuthParameter::Scope, - OAuthParameter::Nonce, + AuthParameter::ClientId, + AuthParameter::ResponseType, + AuthParameter::Scope, + AuthParameter::Nonce, ], )?; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index ff2e7e77..f94893fd 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -14,16 +14,16 @@ use crate::identity::{ AsQuery, Authority, AuthorizationUrl, AzureCloudInstance, OpenIdCredentialBuilder, Prompt, ResponseMode, ResponseType, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use crate::identity::tracing_targets::CREDENTIAL_EXECUTOR; #[cfg(feature = "interactive-auth")] use { - crate::identity::{AuthorizationEvent, AuthorizationResponse}, - crate::web::{ - HostOptions, InteractiveAuth, InteractiveAuthEvent, UserEvents, WebViewHostValidator, - WebViewOptions, + crate::identity::AuthorizationResponse, + crate::interactive::{ + HostOptions, InteractiveAuthEvent, UserEvents, WebViewAuth, WebViewAuthorizationEvent, + WebViewHostValidator, WebViewOptions, }, graph_error::{WebViewError, WebViewResult}, wry::{ @@ -209,7 +209,7 @@ impl OpenIdAuthorizationUrlParameters { &self, client_secret: impl AsRef<str>, web_view_options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<OpenIdCredentialBuilder>> { + ) -> WebViewResult<WebViewAuthorizationEvent<OpenIdCredentialBuilder>> { if self.response_mode.eq(&Some(ResponseMode::FormPost)) { return Err(AF::msg_err( "response_mode", @@ -221,7 +221,7 @@ impl OpenIdAuthorizationUrlParameters { let (sender, receiver) = std::sync::mpsc::channel(); std::thread::spawn(move || { - OpenIdAuthorizationUrlParameters::interactive_auth( + OpenIdAuthorizationUrlParameters::run( uri, vec![redirect_uri], web_view_options, @@ -248,20 +248,22 @@ impl OpenIdAuthorizationUrlParameters { .or(uri.fragment()) .ok_or(WebViewError::InvalidUri(format!( "uri missing query or fragment: {}", - uri.to_string() + uri )))?; let authorization_response: AuthorizationResponse = serde_urlencoded::from_str(query).map_err(|_| { WebViewError::InvalidUri(format!( "unable to deserialize query or fragment: {}", - uri.to_string() + uri )) })?; if authorization_response.is_err() { tracing::debug!(target: "graph_rs_sdk::interactive_auth", "error in authorization query or fragment from redirect uri"); - return Ok(AuthorizationEvent::Unauthorized(authorization_response)); + return Ok(WebViewAuthorizationEvent::Unauthorized( + authorization_response, + )); } tracing::debug!(target: "graph_rs_sdk::interactive_auth", "parsed authorization query or fragment from redirect uri"); @@ -277,13 +279,13 @@ impl OpenIdAuthorizationUrlParameters { credential_builder.with_id_token_verification(true); } - Ok(AuthorizationEvent::Authorized { + Ok(WebViewAuthorizationEvent::Authorized { authorization_response, credential_builder, }) } InteractiveAuthEvent::WindowClosed(window_close_reason) => Ok( - AuthorizationEvent::WindowClosed(window_close_reason.to_string()), + WebViewAuthorizationEvent::WindowClosed(window_close_reason.to_string()), ), }, } @@ -303,7 +305,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { &self, azure_cloud_instance: &AzureCloudInstance, ) -> IdentityResult<Url> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { @@ -378,18 +380,18 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { let query = serializer.encode_query( vec![ - OAuthParameter::ResponseMode, - OAuthParameter::RedirectUri, - OAuthParameter::State, - OAuthParameter::Prompt, - OAuthParameter::LoginHint, - OAuthParameter::DomainHint, + AuthParameter::ResponseMode, + AuthParameter::RedirectUri, + AuthParameter::State, + AuthParameter::Prompt, + AuthParameter::LoginHint, + AuthParameter::DomainHint, ], vec![ - OAuthParameter::ClientId, - OAuthParameter::ResponseType, - OAuthParameter::Scope, - OAuthParameter::Nonce, + AuthParameter::ClientId, + AuthParameter::ResponseType, + AuthParameter::Scope, + AuthParameter::Nonce, ], )?; @@ -400,7 +402,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { } #[cfg(feature = "interactive-auth")] -impl InteractiveAuth for OpenIdAuthorizationUrlParameters { +impl WebViewAuth for OpenIdAuthorizationUrlParameters { fn webview( host_options: HostOptions, window: Window, @@ -493,7 +495,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { &mut self, response_type: I, ) -> &mut Self { - self.credential.response_type = BTreeSet::from_iter(response_type.into_iter()); + self.credential.response_type = BTreeSet::from_iter(response_type); self } @@ -580,7 +582,7 @@ impl OpenIdAuthorizationUrlParameterBuilder { &self, client_secret: impl AsRef<str>, options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<OpenIdCredentialBuilder>> { + ) -> WebViewResult<WebViewAuthorizationEvent<OpenIdCredentialBuilder>> { self.credential .interactive_webview_authentication(client_secret, options) } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 64428245..ac50979f 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -26,7 +26,7 @@ use crate::identity::{ ConfidentialClientApplication, IdToken, OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, TokenCredentialExecutor, }; -use crate::internal::{OAuthParameter, OAuthSerializer}; +use crate::internal::{AuthParameter, AuthSerializer}; credential_builder!( OpenIdCredentialBuilder, @@ -66,7 +66,7 @@ pub struct OpenIdCredential { /// Used only when the client generates the pkce itself when the generate method /// is called. pub(crate) pkce: Option<ProofKeyCodeExchange>, - serializer: OAuthSerializer, + serializer: AuthSerializer, token_cache: InMemoryCacheStore<Token>, verify_id_token: bool, id_token_jwt: Option<DecodedJwt>, @@ -495,11 +495,11 @@ impl TokenCredentialExecutor for OpenIdCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AF::result(OAuthParameter::ClientId.alias()); + return AF::result(AuthParameter::ClientId.alias()); } if self.client_secret.trim().is_empty() { - return AF::result(OAuthParameter::ClientSecret.alias()); + return AF::result(AuthParameter::ClientSecret.alias()); } self.serializer @@ -509,7 +509,7 @@ impl TokenCredentialExecutor for OpenIdCredential { if let Some(refresh_token) = self.refresh_token.as_ref() { if refresh_token.trim().is_empty() { - return AF::msg_result(OAuthParameter::RefreshToken, "Refresh token is empty"); + return AF::msg_result(AuthParameter::RefreshToken, "Refresh token is empty"); } self.serializer @@ -517,18 +517,18 @@ impl TokenCredentialExecutor for OpenIdCredential { .refresh_token(refresh_token.as_ref()); return self.serializer.as_credential_map( - vec![OAuthParameter::Scope], + vec![AuthParameter::Scope], vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RefreshToken, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::ClientSecret, + AuthParameter::RefreshToken, + AuthParameter::GrantType, ], ); } else if let Some(authorization_code) = self.authorization_code.as_ref() { if authorization_code.trim().is_empty() { return AF::msg_result( - OAuthParameter::AuthorizationCode.alias(), + AuthParameter::AuthorizationCode.alias(), "Authorization code is empty", ); } @@ -549,13 +549,13 @@ impl TokenCredentialExecutor for OpenIdCredential { } return self.serializer.as_credential_map( - vec![OAuthParameter::Scope, OAuthParameter::CodeVerifier], + vec![AuthParameter::Scope, AuthParameter::CodeVerifier], vec![ - OAuthParameter::ClientId, - OAuthParameter::ClientSecret, - OAuthParameter::RedirectUri, - OAuthParameter::AuthorizationCode, - OAuthParameter::GrantType, + AuthParameter::ClientId, + AuthParameter::ClientSecret, + AuthParameter::RedirectUri, + AuthParameter::AuthorizationCode, + AuthParameter::GrantType, ], ); } @@ -563,8 +563,8 @@ impl TokenCredentialExecutor for OpenIdCredential { AF::msg_result( format!( "{} or {}", - OAuthParameter::AuthorizationCode.alias(), - OAuthParameter::RefreshToken.alias() + AuthParameter::AuthorizationCode.alias(), + AuthParameter::RefreshToken.alias() ), "Either authorization code or refresh token is required", ) diff --git a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs index 6272f4ef..7183c97d 100644 --- a/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs +++ b/graph-oauth/src/identity/credentials/resource_owner_password_credential.rs @@ -3,7 +3,7 @@ use crate::identity::{ tracing_targets::CREDENTIAL_EXECUTOR, Authority, AzureCloudInstance, Token, TokenCredentialExecutor, }; -use crate::oauth_serializer::{OAuthParameter, OAuthSerializer}; +use crate::oauth_serializer::{AuthParameter, AuthSerializer}; use async_trait::async_trait; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use graph_core::http::{AsyncResponseConverterExt, ResponseConverterExt}; @@ -154,18 +154,18 @@ impl TokenCache for ResourceOwnerPasswordCredential { #[async_trait] impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { fn form_urlencode(&mut self) -> IdentityResult<HashMap<String, String>> { - let mut serializer = OAuthSerializer::new(); + let mut serializer = AuthSerializer::new(); let client_id = self.app_config.client_id.to_string(); if client_id.is_empty() || self.app_config.client_id.is_nil() { - return AF::result(OAuthParameter::ClientId.alias()); + return AF::result(AuthParameter::ClientId.alias()); } if self.username.trim().is_empty() { - return AF::result(OAuthParameter::Username.alias()); + return AF::result(AuthParameter::Username.alias()); } if self.password.trim().is_empty() { - return AF::result(OAuthParameter::Password.alias()); + return AF::result(AuthParameter::Password.alias()); } serializer @@ -174,8 +174,8 @@ impl TokenCredentialExecutor for ResourceOwnerPasswordCredential { .set_scope(self.app_config.scope.clone()); serializer.as_credential_map( - vec![OAuthParameter::Scope], - vec![OAuthParameter::ClientId, OAuthParameter::GrantType], + vec![AuthParameter::Scope], + vec![AuthParameter::ClientId, AuthParameter::GrantType], ) } @@ -269,7 +269,7 @@ impl ResourceOwnerPasswordCredentialBuilder { authority: T, ) -> IdentityResult<&mut Self> { let authority = authority.into(); - if vec![ + if [ Authority::Common, Authority::Consumers, Authority::AzureActiveDirectory, diff --git a/graph-oauth/src/identity/credentials/x509_certificate.rs b/graph-oauth/src/identity/credentials/x509_certificate.rs index 06442f71..5de5eec3 100644 --- a/graph-oauth/src/identity/credentials/x509_certificate.rs +++ b/graph-oauth/src/identity/credentials/x509_certificate.rs @@ -349,7 +349,7 @@ impl X509Certificate { .set_rsa_padding(Padding::PKCS1) .map_err(|err| AF::x509(err.to_string()))?; signer - .update(token.as_str().as_bytes()) + .update(token.as_bytes()) .map_err(|err| AF::x509(err.to_string()))?; let signature = URL_SAFE_NO_PAD.encode( signer @@ -373,7 +373,7 @@ impl X509Certificate { .set_rsa_padding(Padding::PKCS1) .map_err(|err| AF::x509(err.to_string()))?; signer - .update(token.as_str().as_bytes()) + .update(token.as_bytes()) .map_err(|err| AF::x509(err.to_string()))?; let signature = URL_SAFE_NO_PAD.encode( signer diff --git a/graph-oauth/src/identity/device_authorization_response.rs b/graph-oauth/src/identity/device_authorization_response.rs index 4f618734..a89db43b 100644 --- a/graph-oauth/src/identity/device_authorization_response.rs +++ b/graph-oauth/src/identity/device_authorization_response.rs @@ -8,7 +8,7 @@ use serde_json::Value; use graph_core::http::JsonHttpResponse; #[cfg(feature = "interactive-auth")] -use crate::web::WindowCloseReason; +use crate::interactive::WindowCloseReason; #[cfg(feature = "interactive-auth")] use crate::identity::{DeviceCodeCredential, PublicClientApplication}; diff --git a/graph-oauth/src/identity/into_credential_builder.rs b/graph-oauth/src/identity/into_credential_builder.rs new file mode 100644 index 00000000..a5761135 --- /dev/null +++ b/graph-oauth/src/identity/into_credential_builder.rs @@ -0,0 +1,8 @@ +use std::fmt::Debug; + +pub trait IntoCredentialBuilder<CredentialBuilder: Clone + Debug> { + type Response; + type Error: std::error::Error; + + fn into_credential_builder(self) -> Result<(Self::Response, CredentialBuilder), Self::Error>; +} diff --git a/graph-oauth/src/identity/mod.rs b/graph-oauth/src/identity/mod.rs index af68c5f6..0f1f6d64 100644 --- a/graph-oauth/src/identity/mod.rs +++ b/graph-oauth/src/identity/mod.rs @@ -7,6 +7,7 @@ mod authorization_url; mod credentials; mod device_authorization_response; mod id_token; +mod into_credential_builder; mod token; #[cfg(feature = "openssl")] @@ -24,4 +25,5 @@ pub use authorization_url::*; pub use credentials::*; pub use device_authorization_response::*; pub use id_token::*; +pub use into_credential_builder::*; pub use token::*; diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 9a1427c2..14ffc1ab 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -475,7 +475,7 @@ impl TryFrom<AuthorizationResponse> for Token { impl Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.access_token.to_string()) + write!(f, "{}", self.access_token) } } diff --git a/graph-oauth/src/web/interactive_auth.rs b/graph-oauth/src/interactive/interactive_auth.rs similarity index 91% rename from graph-oauth/src/web/interactive_auth.rs rename to graph-oauth/src/interactive/interactive_auth.rs index 5e402775..af25c7fb 100644 --- a/graph-oauth/src/web/interactive_auth.rs +++ b/graph-oauth/src/interactive/interactive_auth.rs @@ -1,5 +1,5 @@ use crate::identity::tracing_targets::INTERACTIVE_AUTH; -use crate::web::{HostOptions, WebViewOptions}; +use crate::interactive::{HostOptions, WebViewOptions}; use std::fmt::{Debug, Display, Formatter}; use std::sync::mpsc::Sender; use std::time::{Duration, Instant}; @@ -12,9 +12,6 @@ use wry::webview::WebView; #[cfg(target_family = "unix")] use wry::application::platform::unix::EventLoopBuilderExtUnix; -use crate::identity::AuthorizationEvent; - -use graph_error::WebViewResult; #[cfg(target_family = "windows")] use wry::application::platform::windows::EventLoopBuilderExtWindows; @@ -52,7 +49,7 @@ pub enum UserEvents { ReachedRedirectUri(Url), } -pub trait InteractiveAuth +pub trait WebViewAuth where Self: Debug, { @@ -62,7 +59,7 @@ where proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView>; - fn interactive_auth( + fn run( start_url: Url, redirect_uris: Vec<Url>, options: WebViewOptions, @@ -199,23 +196,3 @@ where .build() } } - -pub trait WithInteractiveAuth<T> { - type CredentialBuilder: Clone + Debug; - - fn with_interactive_auth( - &self, - auth_type: T, - options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<Self::CredentialBuilder>>; -} - -pub trait WithInteractiveAuthBuilder<CredentialBuilder: Clone + Debug> { - type AuthType; - - fn with_interactive_auth_builder( - &self, - auth_type: Self::AuthType, - options: WebViewOptions, - ) -> WebViewResult<AuthorizationEvent<CredentialBuilder>>; -} diff --git a/graph-oauth/src/web/mod.rs b/graph-oauth/src/interactive/mod.rs similarity index 58% rename from graph-oauth/src/web/mod.rs rename to graph-oauth/src/interactive/mod.rs index 4712be3c..e58a5dae 100644 --- a/graph-oauth/src/web/mod.rs +++ b/graph-oauth/src/interactive/mod.rs @@ -1,9 +1,13 @@ mod interactive_auth; +mod webview_authorization_event; mod webview_host_validator; mod webview_options; +mod with_interactive_auth; #[allow(unused_imports)] pub use webview_host_validator::*; pub use interactive_auth::*; +pub use webview_authorization_event::*; pub use webview_options::*; +pub use with_interactive_auth::*; diff --git a/graph-oauth/src/interactive/webview_authorization_event.rs b/graph-oauth/src/interactive/webview_authorization_event.rs new file mode 100644 index 00000000..a3c7416b --- /dev/null +++ b/graph-oauth/src/interactive/webview_authorization_event.rs @@ -0,0 +1,76 @@ +use crate::{AuthorizationResponse, IntoCredentialBuilder}; +use graph_error::{WebViewError, WebViewResult}; +use std::fmt::Debug; + +#[derive(Clone, Debug)] +pub enum WebViewAuthorizationEvent<CredentialBuilder: Clone + Debug> { + Authorized { + authorization_response: AuthorizationResponse, + credential_builder: CredentialBuilder, + }, + Unauthorized(AuthorizationResponse), + WindowClosed(String), +} + +impl<CredentialBuilder: Clone + Debug> IntoCredentialBuilder<CredentialBuilder> + for WebViewAuthorizationEvent<CredentialBuilder> +{ + type Response = AuthorizationResponse; + type Error = WebViewError; + + fn into_credential_builder(self) -> Result<(Self::Response, CredentialBuilder), Self::Error> { + match self { + WebViewAuthorizationEvent::Authorized { + authorization_response, + credential_builder, + } => Ok((authorization_response, credential_builder)), + WebViewAuthorizationEvent::Unauthorized(authorization_response) => { + Err(WebViewError::Authorization { + error: authorization_response + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: authorization_response.error_description.unwrap_or_default(), + error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), + }) + } + WebViewAuthorizationEvent::WindowClosed(reason) => { + Err(WebViewError::WindowClosed(reason)) + } + } + } +} + +impl<CredentialBuilder: Clone + Debug> IntoCredentialBuilder<CredentialBuilder> + for WebViewResult<WebViewAuthorizationEvent<CredentialBuilder>> +{ + type Response = AuthorizationResponse; + type Error = WebViewError; + + fn into_credential_builder(self) -> Result<(Self::Response, CredentialBuilder), Self::Error> { + match self { + Ok(auth_event) => match auth_event { + WebViewAuthorizationEvent::Authorized { + authorization_response, + credential_builder, + } => Ok((authorization_response, credential_builder)), + WebViewAuthorizationEvent::Unauthorized(authorization_response) => { + Err(WebViewError::Authorization { + error: authorization_response + .error + .map(|query_error| query_error.to_string()) + .unwrap_or_default(), + error_description: authorization_response + .error_description + .unwrap_or_default(), + error_uri: authorization_response.error_uri.map(|uri| uri.to_string()), + }) + } + WebViewAuthorizationEvent::WindowClosed(reason) => { + Err(WebViewError::WindowClosed(reason)) + } + }, + Err(err) => Err(err), + } + } +} diff --git a/graph-oauth/src/web/webview_host_validator.rs b/graph-oauth/src/interactive/webview_host_validator.rs similarity index 98% rename from graph-oauth/src/web/webview_host_validator.rs rename to graph-oauth/src/interactive/webview_host_validator.rs index 9f50aeee..7018d6bc 100644 --- a/graph-oauth/src/web/webview_host_validator.rs +++ b/graph-oauth/src/interactive/webview_host_validator.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use url::Url; -use crate::web::HostOptions; +use crate::interactive::HostOptions; use graph_error::{WebViewError, WebViewResult}; pub(crate) struct WebViewHostValidator { diff --git a/graph-oauth/src/web/webview_options.rs b/graph-oauth/src/interactive/webview_options.rs similarity index 100% rename from graph-oauth/src/web/webview_options.rs rename to graph-oauth/src/interactive/webview_options.rs diff --git a/graph-oauth/src/interactive/with_interactive_auth.rs b/graph-oauth/src/interactive/with_interactive_auth.rs new file mode 100644 index 00000000..68afcab7 --- /dev/null +++ b/graph-oauth/src/interactive/with_interactive_auth.rs @@ -0,0 +1,13 @@ +use crate::interactive::{WebViewAuthorizationEvent, WebViewOptions}; +use graph_error::WebViewResult; +use std::fmt::Debug; + +pub trait WithInteractiveAuth<T> { + type CredentialBuilder: Clone + Debug; + + fn with_interactive_auth( + &self, + auth_type: T, + options: WebViewOptions, + ) -> WebViewResult<WebViewAuthorizationEvent<Self::CredentialBuilder>>; +} diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index f3e77654..2f148dfa 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -51,7 +51,7 @@ pub(crate) mod oauth_serializer; pub(crate) mod identity; #[cfg(feature = "interactive-auth")] -pub mod web; +pub mod interactive; pub(crate) mod internal { pub use crate::oauth_serializer::*; diff --git a/graph-oauth/src/oauth_serializer.rs b/graph-oauth/src/oauth_serializer.rs index 71fee919..fd01f126 100644 --- a/graph-oauth/src/oauth_serializer.rs +++ b/graph-oauth/src/oauth_serializer.rs @@ -15,7 +15,7 @@ use crate::strum::IntoEnumIterator; #[derive( Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, EnumIter, )] -pub enum OAuthParameter { +pub enum AuthParameter { ClientId, ClientSecret, RedirectUri, @@ -45,61 +45,61 @@ pub enum OAuthParameter { DeviceCode, } -impl OAuthParameter { +impl AuthParameter { pub fn alias(self) -> &'static str { match self { - OAuthParameter::ClientId => "client_id", - OAuthParameter::ClientSecret => "client_secret", - OAuthParameter::RedirectUri => "redirect_uri", - OAuthParameter::AuthorizationCode => "code", - OAuthParameter::AccessToken => "access_token", - OAuthParameter::RefreshToken => "refresh_token", - OAuthParameter::ResponseMode => "response_mode", - OAuthParameter::ResponseType => "response_type", - OAuthParameter::State => "state", - OAuthParameter::SessionState => "session_state", - OAuthParameter::GrantType => "grant_type", - OAuthParameter::Nonce => "nonce", - OAuthParameter::Prompt => "prompt", - OAuthParameter::IdToken => "id_token", - OAuthParameter::Resource => "resource", - OAuthParameter::DomainHint => "domain_hint", - OAuthParameter::Scope => "scope", - OAuthParameter::LoginHint => "login_hint", - OAuthParameter::ClientAssertion => "client_assertion", - OAuthParameter::ClientAssertionType => "client_assertion_type", - OAuthParameter::CodeVerifier => "code_verifier", - OAuthParameter::CodeChallenge => "code_challenge", - OAuthParameter::CodeChallengeMethod => "code_challenge_method", - OAuthParameter::AdminConsent => "admin_consent", - OAuthParameter::Username => "username", - OAuthParameter::Password => "password", - OAuthParameter::DeviceCode => "device_code", + AuthParameter::ClientId => "client_id", + AuthParameter::ClientSecret => "client_secret", + AuthParameter::RedirectUri => "redirect_uri", + AuthParameter::AuthorizationCode => "code", + AuthParameter::AccessToken => "access_token", + AuthParameter::RefreshToken => "refresh_token", + AuthParameter::ResponseMode => "response_mode", + AuthParameter::ResponseType => "response_type", + AuthParameter::State => "state", + AuthParameter::SessionState => "session_state", + AuthParameter::GrantType => "grant_type", + AuthParameter::Nonce => "nonce", + AuthParameter::Prompt => "prompt", + AuthParameter::IdToken => "id_token", + AuthParameter::Resource => "resource", + AuthParameter::DomainHint => "domain_hint", + AuthParameter::Scope => "scope", + AuthParameter::LoginHint => "login_hint", + AuthParameter::ClientAssertion => "client_assertion", + AuthParameter::ClientAssertionType => "client_assertion_type", + AuthParameter::CodeVerifier => "code_verifier", + AuthParameter::CodeChallenge => "code_challenge", + AuthParameter::CodeChallengeMethod => "code_challenge_method", + AuthParameter::AdminConsent => "admin_consent", + AuthParameter::Username => "username", + AuthParameter::Password => "password", + AuthParameter::DeviceCode => "device_code", } } fn is_debug_redacted(&self) -> bool { matches!( self, - OAuthParameter::ClientId - | OAuthParameter::ClientSecret - | OAuthParameter::AccessToken - | OAuthParameter::RefreshToken - | OAuthParameter::IdToken - | OAuthParameter::CodeVerifier - | OAuthParameter::CodeChallenge - | OAuthParameter::Password + AuthParameter::ClientId + | AuthParameter::ClientSecret + | AuthParameter::AccessToken + | AuthParameter::RefreshToken + | AuthParameter::IdToken + | AuthParameter::CodeVerifier + | AuthParameter::CodeChallenge + | AuthParameter::Password ) } } -impl Display for OAuthParameter { +impl Display for AuthParameter { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.alias().to_string()) + write!(f, "{}", self.alias()) } } -impl AsRef<str> for OAuthParameter { +impl AsRef<str> for AuthParameter { fn as_ref(&self) -> &'static str { self.alias() } @@ -112,27 +112,27 @@ impl AsRef<str> for OAuthParameter { /// /// # Example /// ``` -/// use graph_oauth::extensions::OAuthSerializer; -/// let oauth = OAuthSerializer::new(); +/// use graph_oauth::extensions::AuthSerializer; +/// let oauth = AuthSerializer::new(); /// ``` #[derive(Default, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct OAuthSerializer { +pub struct AuthSerializer { scopes: BTreeSet<String>, parameters: BTreeMap<String, String>, log_pii: bool, } -impl OAuthSerializer { +impl AuthSerializer { /// Create a new OAuth instance. /// /// # Example /// ``` - /// use graph_oauth::extensions::OAuthSerializer; + /// use graph_oauth::extensions::AuthSerializer; /// - /// let mut oauth = OAuthSerializer::new(); + /// let mut oauth = AuthSerializer::new(); /// ``` - pub fn new() -> OAuthSerializer { - OAuthSerializer { + pub fn new() -> AuthSerializer { + AuthSerializer { scopes: BTreeSet::new(), parameters: BTreeMap::new(), log_pii: false, @@ -146,14 +146,14 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); - /// oauth.insert(OAuthParameter::AuthorizationCode, "code"); - /// assert!(oauth.contains(OAuthParameter::AuthorizationCode)); - /// println!("{:#?}", oauth.get(OAuthParameter::AuthorizationCode)); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); + /// oauth.insert(AuthParameter::AuthorizationCode, "code"); + /// assert!(oauth.contains(AuthParameter::AuthorizationCode)); + /// println!("{:#?}", oauth.get(AuthParameter::AuthorizationCode)); /// ``` - pub fn insert<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut OAuthSerializer { + pub fn insert<V: ToString>(&mut self, oac: AuthParameter, value: V) -> &mut AuthSerializer { self.parameters.insert(oac.to_string(), value.to_string()); self } @@ -164,13 +164,13 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); - /// let entry = oauth.entry_with(OAuthParameter::AuthorizationCode, "code"); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); + /// let entry = oauth.entry_with(AuthParameter::AuthorizationCode, "code"); /// assert_eq!(entry, "code") /// ``` - pub fn entry_with<V: ToString>(&mut self, oac: OAuthParameter, value: V) -> &mut String { + pub fn entry_with<V: ToString>(&mut self, oac: AuthParameter, value: V) -> &mut String { self.parameters .entry(oac.alias().to_string()) .or_insert_with(|| value.to_string()) @@ -181,7 +181,7 @@ impl OAuthSerializer { /// This `enum` is constructed from the [`entry`] method on [`BTreeMap`]. /// /// [`entry`]: BTreeMap::entry - pub fn entry<V: ToString>(&mut self, oac: OAuthParameter) -> Entry<String, String> { + pub fn entry<V: ToString>(&mut self, oac: AuthParameter) -> Entry<String, String> { self.parameters.entry(oac.alias().to_string()) } @@ -189,14 +189,14 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); /// oauth.authorization_code("code"); - /// let code = oauth.get(OAuthParameter::AuthorizationCode); + /// let code = oauth.get(AuthParameter::AuthorizationCode); /// assert_eq!("code", code.unwrap().as_str()); /// ``` - pub fn get(&self, oac: OAuthParameter) -> Option<String> { + pub fn get(&self, oac: AuthParameter) -> Option<String> { self.parameters.get(oac.alias()).cloned() } @@ -204,13 +204,13 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); - /// println!("{:#?}", oauth.contains(OAuthParameter::Nonce)); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); + /// println!("{:#?}", oauth.contains(AuthParameter::Nonce)); /// ``` - pub fn contains(&self, t: OAuthParameter) -> bool { - if t == OAuthParameter::Scope { + pub fn contains(&self, t: AuthParameter) -> bool { + if t == AuthParameter::Scope { return !self.scopes.is_empty(); } self.parameters.contains_key(t.alias()) @@ -224,17 +224,17 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); /// oauth.client_id("client_id"); /// - /// assert_eq!(oauth.contains(OAuthParameter::ClientId), true); - /// oauth.remove(OAuthParameter::ClientId); + /// assert_eq!(oauth.contains(AuthParameter::ClientId), true); + /// oauth.remove(AuthParameter::ClientId); /// - /// assert_eq!(oauth.contains(OAuthParameter::ClientId), false); + /// assert_eq!(oauth.contains(AuthParameter::ClientId), false); /// ``` - pub fn remove(&mut self, oac: OAuthParameter) -> &mut OAuthSerializer { + pub fn remove(&mut self, oac: AuthParameter) -> &mut AuthSerializer { self.parameters.remove(oac.alias()); self } @@ -243,301 +243,301 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); /// oauth.client_id("client_id"); /// ``` - pub fn client_id(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ClientId, value) + pub fn client_id(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::ClientId, value) } /// Set the state for an OAuth request. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # use graph_oauth::extensions::OAuthParameter; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # use graph_oauth::extensions::AuthParameter; + /// # let mut oauth = AuthSerializer::new(); /// oauth.state("1234"); /// ``` - pub fn state(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::State, value) + pub fn state(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::State, value) } /// Set the client secret for an OAuth request. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.client_secret("client_secret"); /// ``` - pub fn client_secret(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ClientSecret, value) + pub fn client_secret(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::ClientSecret, value) } /// Set the redirect url of a request /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.redirect_uri("https://localhost:8888/redirect"); /// ``` - pub fn redirect_uri(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::RedirectUri, value) + pub fn redirect_uri(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::RedirectUri, value) } /// Set the access code. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut serializer = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut serializer = AuthSerializer::new(); /// serializer.authorization_code("LDSF[POK43"); /// ``` - pub fn authorization_code(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::AuthorizationCode, value) + pub fn authorization_code(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::AuthorizationCode, value) } /// Set the response mode. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut serializer = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut serializer = AuthSerializer::new(); /// serializer.response_mode("query"); /// ``` - pub fn response_mode(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ResponseMode, value) + pub fn response_mode(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::ResponseMode, value) } /// Set the response type. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.response_type("token"); /// ``` - pub fn response_type<T: ToString>(&mut self, value: T) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ResponseType, value) + pub fn response_type<T: ToString>(&mut self, value: T) -> &mut AuthSerializer { + self.insert(AuthParameter::ResponseType, value) } pub fn response_types( &mut self, value: std::collections::btree_set::Iter<'_, ResponseType>, - ) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ResponseType, value.as_query()) + ) -> &mut AuthSerializer { + self.insert(AuthParameter::ResponseType, value.as_query()) } /// Set the nonce. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::AuthSerializer; /// - /// # let mut oauth = OAuthSerializer::new(); + /// # let mut oauth = AuthSerializer::new(); /// oauth.nonce("1234"); /// ``` - pub fn nonce(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Nonce, value) + pub fn nonce(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::Nonce, value) } /// Set the prompt for open id. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::AuthSerializer; /// - /// # let mut oauth = OAuthSerializer::new(); + /// # let mut oauth = AuthSerializer::new(); /// oauth.prompt("login"); /// ``` - pub fn prompt(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Prompt, value) + pub fn prompt(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::Prompt, value) } - pub fn prompts(&mut self, value: &[Prompt]) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Prompt, value.to_vec().as_query()) + pub fn prompts(&mut self, value: &[Prompt]) -> &mut AuthSerializer { + self.insert(AuthParameter::Prompt, value.to_vec().as_query()) } /// Set the session state. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.session_state("session-state"); /// ``` - pub fn session_state(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::SessionState, value) + pub fn session_state(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::SessionState, value) } /// Set the grant_type. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.grant_type("token"); /// ``` - pub fn grant_type(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::GrantType, value) + pub fn grant_type(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::GrantType, value) } /// Set the resource. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.resource("resource"); /// ``` - pub fn resource(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Resource, value) + pub fn resource(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::Resource, value) } /// Set the code verifier. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.code_verifier("code_verifier"); /// ``` - pub fn code_verifier(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::CodeVerifier, value) + pub fn code_verifier(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::CodeVerifier, value) } /// Set the domain hint. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.domain_hint("domain_hint"); /// ``` - pub fn domain_hint(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::DomainHint, value) + pub fn domain_hint(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::DomainHint, value) } /// Set the code challenge. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.code_challenge("code_challenge"); /// ``` - pub fn code_challenge(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::CodeChallenge, value) + pub fn code_challenge(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::CodeChallenge, value) } /// Set the code challenge method. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.code_challenge_method("code_challenge_method"); /// ``` - pub fn code_challenge_method(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::CodeChallengeMethod, value) + pub fn code_challenge_method(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::CodeChallengeMethod, value) } /// Set the login hint. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.login_hint("login_hint"); /// ``` - pub fn login_hint(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::LoginHint, value) + pub fn login_hint(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::LoginHint, value) } /// Set the client assertion. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.client_assertion("client_assertion"); /// ``` - pub fn client_assertion(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ClientAssertion, value) + pub fn client_assertion(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::ClientAssertion, value) } /// Set the client assertion type. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// oauth.client_assertion_type("client_assertion_type"); /// ``` - pub fn client_assertion_type(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::ClientAssertionType, value) + pub fn client_assertion_type(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::ClientAssertionType, value) } /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::{AuthSerializer, AuthParameter}; + /// # let mut oauth = AuthSerializer::new(); /// oauth.username("user"); - /// assert!(oauth.contains(OAuthParameter::Username)) + /// assert!(oauth.contains(AuthParameter::Username)) /// ``` - pub fn username(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Username, value) + pub fn username(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::Username, value) } /// Set the redirect uri that user will be redirected to after logging out. /// /// # Example /// ``` - /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::{AuthSerializer, AuthParameter}; + /// # let mut oauth = AuthSerializer::new(); /// oauth.password("user"); - /// assert!(oauth.contains(OAuthParameter::Password)) + /// assert!(oauth.contains(AuthParameter::Password)) /// ``` - pub fn password(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::Password, value) + pub fn password(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::Password, value) } - pub fn refresh_token(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::RefreshToken, value) + pub fn refresh_token(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::RefreshToken, value) } /// Set the device code for the device authorization flow. /// /// # Example /// ``` - /// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::{AuthSerializer, AuthParameter}; + /// # let mut oauth = AuthSerializer::new(); /// oauth.device_code("device_code"); - /// assert!(oauth.contains(OAuthParameter::DeviceCode)) + /// assert!(oauth.contains(AuthParameter::DeviceCode)) /// ``` - pub fn device_code(&mut self, value: &str) -> &mut OAuthSerializer { - self.insert(OAuthParameter::DeviceCode, value) + pub fn device_code(&mut self, value: &str) -> &mut AuthSerializer { + self.insert(AuthParameter::DeviceCode, value) } /// Add a scope' for the OAuth URL. /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// /// oauth.add_scope("Sites.Read") /// .add_scope("Sites.ReadWrite") /// .add_scope("Sites.ReadWrite.All"); /// assert_eq!(oauth.join_scopes(" "), "Sites.Read Sites.ReadWrite Sites.ReadWrite.All"); /// ``` - pub fn add_scope<T: ToString>(&mut self, scope: T) -> &mut OAuthSerializer { + pub fn add_scope<T: ToString>(&mut self, scope: T) -> &mut AuthSerializer { self.scopes.insert(scope.to_string()); self } @@ -546,8 +546,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// let mut oauth = AuthSerializer::new(); /// oauth.add_scope("Files.Read"); /// oauth.add_scope("Files.ReadWrite"); /// @@ -563,8 +563,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// /// // the scopes take a separator just like Vec join. /// let s = oauth.join_scopes(" "); @@ -582,9 +582,9 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::AuthSerializer; /// # use std::collections::HashSet; - /// # let mut oauth = OAuthSerializer::new(); + /// # let mut oauth = AuthSerializer::new(); /// /// let scopes = vec!["Files.Read", "Files.ReadWrite"]; /// oauth.extend_scopes(&scopes); @@ -600,9 +600,9 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; + /// # use graph_oauth::extensions::AuthSerializer; /// # use std::collections::HashSet; - /// # let mut oauth = OAuthSerializer::new(); + /// # let mut oauth = AuthSerializer::new(); /// /// let scopes = vec!["Files.Read", "Files.ReadWrite"]; /// oauth.extend_scopes(&scopes); @@ -618,8 +618,8 @@ impl OAuthSerializer { /// /// # Example /// ``` - /// # use graph_oauth::extensions::OAuthSerializer; - /// # let mut oauth = OAuthSerializer::new(); + /// # use graph_oauth::extensions::AuthSerializer; + /// # let mut oauth = AuthSerializer::new(); /// /// oauth.add_scope("Files.Read"); /// assert_eq!(oauth.contains_scope("Files.Read"), true); @@ -633,9 +633,9 @@ impl OAuthSerializer { } } -impl OAuthSerializer { - fn try_as_tuple(&self, oac: &OAuthParameter) -> IdentityResult<(String, String)> { - if oac.eq(&OAuthParameter::Scope) { +impl AuthSerializer { + fn try_as_tuple(&self, oac: &AuthParameter) -> IdentityResult<(String, String)> { + if oac.eq(&AuthParameter::Scope) { if self.scopes.is_empty() { return Err(AuthorizationFailure::required(oac)); } @@ -650,8 +650,8 @@ impl OAuthSerializer { pub fn encode_query( &mut self, - optional_fields: Vec<OAuthParameter>, - required_fields: Vec<OAuthParameter>, + optional_fields: Vec<AuthParameter>, + required_fields: Vec<AuthParameter>, ) -> IdentityResult<String> { let mut serializer = Serializer::new(String::new()); for parameter in required_fields { @@ -683,8 +683,8 @@ impl OAuthSerializer { pub fn as_credential_map( &mut self, - optional_fields: Vec<OAuthParameter>, - required_fields: Vec<OAuthParameter>, + optional_fields: Vec<AuthParameter>, + required_fields: Vec<AuthParameter>, ) -> IdentityResult<HashMap<String, String>> { let mut required_map = required_fields .iter() @@ -705,32 +705,32 @@ impl OAuthSerializer { /// /// # Example /// ``` -/// # use graph_oauth::extensions::{OAuthSerializer, OAuthParameter}; +/// # use graph_oauth::extensions::{AuthSerializer, AuthParameter}; /// # use std::collections::HashMap; -/// # let mut oauth = OAuthSerializer::new(); -/// let mut map: HashMap<OAuthParameter, &str> = HashMap::new(); -/// map.insert(OAuthParameter::ClientId, "client_id"); -/// map.insert(OAuthParameter::ClientSecret, "client_secret"); +/// # let mut oauth = AuthSerializer::new(); +/// let mut map: HashMap<AuthParameter, &str> = HashMap::new(); +/// map.insert(AuthParameter::ClientId, "client_id"); +/// map.insert(AuthParameter::ClientSecret, "client_secret"); /// /// oauth.extend(map); -/// # assert_eq!(oauth.get(OAuthParameter::ClientId), Some("client_id".to_string())); -/// # assert_eq!(oauth.get(OAuthParameter::ClientSecret), Some("client_secret".to_string())); +/// # assert_eq!(oauth.get(AuthParameter::ClientId), Some("client_id".to_string())); +/// # assert_eq!(oauth.get(AuthParameter::ClientSecret), Some("client_secret".to_string())); /// ``` -impl<V: ToString> Extend<(OAuthParameter, V)> for OAuthSerializer { - fn extend<I: IntoIterator<Item = (OAuthParameter, V)>>(&mut self, iter: I) { +impl<V: ToString> Extend<(AuthParameter, V)> for AuthSerializer { + fn extend<I: IntoIterator<Item = (AuthParameter, V)>>(&mut self, iter: I) { iter.into_iter().for_each(|entry| { self.insert(entry.0, entry.1); }); } } -impl fmt::Debug for OAuthSerializer { +impl fmt::Debug for AuthSerializer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut map_debug: BTreeMap<&str, &str> = BTreeMap::new(); for (key, value) in self.parameters.iter() { if self.log_pii { map_debug.insert(key.as_str(), value.as_str()); - } else if let Some(oac) = OAuthParameter::iter() + } else if let Some(oac) = AuthParameter::iter() .find(|oac| oac.alias().eq(key.as_str()) && oac.is_debug_redacted()) { map_debug.insert(oac.alias(), "[REDACTED]"); @@ -745,132 +745,3 @@ impl fmt::Debug for OAuthSerializer { .finish() } } - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn oauth_parameters_from_credential() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("client_id") - .client_secret("client_secret") - .redirect_uri("https://example.com/redirect?") - .authorization_code("ADSLFJL4L3") - .response_mode("response_mode") - .response_type("response_type") - .state("state") - .grant_type("grant_type") - .nonce("nonce") - .prompt("login") - .session_state("session_state") - .client_assertion("client_assertion") - .client_assertion_type("client_assertion_type") - .code_verifier("code_verifier") - .login_hint("login_hint") - .domain_hint("domain_hint") - .resource("resource"); - - OAuthParameter::iter().for_each(|credential| { - if oauth.contains(credential) { - match credential { - OAuthParameter::ClientId => { - assert_eq!(oauth.get(credential), Some("client_id".into())) - } - OAuthParameter::ClientSecret => { - assert_eq!(oauth.get(credential), Some("client_secret".into())) - } - OAuthParameter::RedirectUri => assert_eq!( - oauth.get(credential), - Some("https://example.com/redirect?".into()) - ), - OAuthParameter::AuthorizationCode => { - assert_eq!(oauth.get(credential), Some("ADSLFJL4L3".into())) - } - OAuthParameter::ResponseMode => { - assert_eq!(oauth.get(credential), Some("response_mode".into())) - } - OAuthParameter::ResponseType => { - assert_eq!(oauth.get(credential), Some("response_type".into())) - } - OAuthParameter::State => { - assert_eq!(oauth.get(credential), Some("state".into())) - } - OAuthParameter::GrantType => { - assert_eq!(oauth.get(credential), Some("grant_type".into())) - } - OAuthParameter::Nonce => { - assert_eq!(oauth.get(credential), Some("nonce".into())) - } - OAuthParameter::Prompt => { - assert_eq!(oauth.get(credential), Some("login".into())) - } - OAuthParameter::SessionState => { - assert_eq!(oauth.get(credential), Some("session_state".into())) - } - OAuthParameter::ClientAssertion => { - assert_eq!(oauth.get(credential), Some("client_assertion".into())) - } - OAuthParameter::ClientAssertionType => { - assert_eq!(oauth.get(credential), Some("client_assertion_type".into())) - } - OAuthParameter::CodeVerifier => { - assert_eq!(oauth.get(credential), Some("code_verifier".into())) - } - OAuthParameter::Resource => { - assert_eq!(oauth.get(credential), Some("resource".into())) - } - _ => {} - } - } - }); - } - - #[test] - fn remove_credential() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("bb301aaa-1201-4259-a230923fds32") - .redirect_uri("http://localhost:8888/redirect") - .client_secret("CLDIE3F") - .authorization_code("ALDSKFJLKERLKJALSDKJF2209LAKJGFL"); - assert!(oauth.get(OAuthParameter::ClientId).is_some()); - oauth.remove(OAuthParameter::ClientId); - assert!(oauth.get(OAuthParameter::ClientId).is_none()); - oauth.client_id("client_id"); - assert!(oauth.get(OAuthParameter::ClientId).is_some()); - - assert!(oauth.get(OAuthParameter::RedirectUri).is_some()); - oauth.remove(OAuthParameter::RedirectUri); - assert!(oauth.get(OAuthParameter::RedirectUri).is_none()); - } - - #[test] - fn setters() { - // Doesn't matter the flow here as this is for testing - // that the credentials are entered/retrieved correctly. - let mut oauth = OAuthSerializer::new(); - oauth - .client_id("client_id") - .client_secret("client_secret") - .redirect_uri("https://example.com/redirect") - .authorization_code("access_code"); - - let test_setter = |c: OAuthParameter, s: &str| { - let result = oauth.get(c); - assert!(result.is_some()); - assert!(result.is_some()); - assert_eq!(result.unwrap(), s); - }; - - test_setter(OAuthParameter::ClientId, "client_id"); - test_setter(OAuthParameter::ClientSecret, "client_secret"); - test_setter(OAuthParameter::RedirectUri, "https://example.com/redirect"); - test_setter(OAuthParameter::AuthorizationCode, "access_code"); - } -} diff --git a/src/client/graph.rs b/src/client/graph.rs index 8511747f..61b43550 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -280,11 +280,6 @@ impl GraphClient { /// assert_eq!(client.url().to_string(), "https://graph.microsoft.com/v1.0".to_string()) /// ``` pub fn use_endpoint(&mut self, url: &Url) { - if cfg!(feature = "test-util") { - self.endpoint = url.clone(); - return; - } - if url.query().is_some() { panic!( "Invalid query - provide only the scheme, host, and optional path of the Uri such as https://graph.microsoft.com/v1.0" @@ -299,6 +294,11 @@ impl GraphClient { } } + #[cfg(feature = "test-util")] + pub fn use_test_endpoint(&mut self, url: &Url) { + self.endpoint = url.clone(); + } + pub fn decoded_jwt(&self) -> Option<&DecodedJwt> { self.client.get_decoded_jwt() } @@ -687,7 +687,7 @@ mod test { #[test] fn try_valid_hosts() { - let urls = vec![ + let urls = [ "https://graph.microsoft.com/v1.0", "https://graph.microsoft.us", "https://dod-graph.microsoft.us", @@ -704,3 +704,63 @@ mod test { } } } + +#[cfg(test)] +#[cfg(feature = "test-util")] +mod test_util_feature { + use crate::{http::Url, Graph, GraphClientConfiguration, ODataQuery}; + use wiremock::matchers::{bearer_token, method, path, query_param}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + /// Tests the test-util feature and setting https-only to false. + #[tokio::test] + async fn can_set_test_endpoint() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/users")) + .and(query_param("$top", "10")) + .and(bearer_token("token")) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; + + let graph_client_configuration = GraphClientConfiguration::new() + .access_token("token") + .https_only(false); + + let mut client = Graph::from(graph_client_configuration); + let uri = mock_server.uri(); + client.use_test_endpoint(&Url::parse(uri.as_str()).unwrap()); + + let response = client.users().list_user().top("10").send().await.unwrap(); + let status = response.status(); + assert_eq!(status.as_u16(), 200); + } + + #[tokio::test] + #[should_panic] + async fn test_util_feature_use_endpoint_panics() { + let mock_server = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/users")) + .and(query_param("$top", "10")) + .and(bearer_token("token")) + .respond_with(ResponseTemplate::new(200)) + .mount(&mock_server) + .await; + + let graph_client_configuration = GraphClientConfiguration::new() + .access_token("token") + .https_only(false); + + let mut client = Graph::from(graph_client_configuration); + let uri = mock_server.uri(); + client.use_endpoint(&Url::parse(uri.as_str()).unwrap()); + + let response = client.users().list_user().top("10").send().await.unwrap(); + let status = response.status(); + assert_eq!(status.as_u16(), 200); + } +} diff --git a/tests/test-util-feature.rs b/tests/test-util-feature.rs deleted file mode 100644 index 74f5a4b8..00000000 --- a/tests/test-util-feature.rs +++ /dev/null @@ -1,29 +0,0 @@ -use graph_rs_sdk::{http::Url, Graph, GraphClientConfiguration, ODataQuery}; -use wiremock::matchers::{bearer_token, method, path, query_param}; -use wiremock::{Mock, MockServer, ResponseTemplate}; - -/// Tests the test-util feature and setting https-only to false. -#[tokio::test] -async fn test_util_feature() { - let mock_server = MockServer::start().await; - - Mock::given(method("GET")) - .and(path("/users")) - .and(query_param("$top", "10")) - .and(bearer_token("token")) - .respond_with(ResponseTemplate::new(200)) - .mount(&mock_server) - .await; - - let graph_client_configuration = GraphClientConfiguration::new() - .access_token("token") - .https_only(false); - - let mut client = Graph::from(graph_client_configuration); - let uri = mock_server.uri(); - client.use_endpoint(&Url::parse(uri.as_str()).unwrap()); - - let response = client.users().list_user().top("10").send().await.unwrap(); - let status = response.status(); - assert_eq!(status.as_u16(), 200); -} diff --git a/tests/todo_tasks_request.rs b/tests/todo_tasks_request.rs index 80ccc53f..9fc321c9 100644 --- a/tests/todo_tasks_request.rs +++ b/tests/todo_tasks_request.rs @@ -31,7 +31,6 @@ async fn list_todo_list_tasks() { .send() .await .unwrap(); - println!("{:#?}\n", response); assert!(response.status().is_success()); let body: TodoListsTasks = response.json().await.unwrap(); assert!(body.value.len() >= 2); diff --git a/tests/upload_session_request.rs b/tests/upload_session_request.rs index 22750a86..b1b792e7 100644 --- a/tests/upload_session_request.rs +++ b/tests/upload_session_request.rs @@ -96,7 +96,7 @@ async fn channel_upload_session(mut upload_session: UploadSession) -> GraphResul } Err(err) => { cancel_request.send().await?; - return Err(err).map_err(GraphFailure::from); + return Err(GraphFailure::from(err)); } } } From 8cea1f175546df824a63f7cb6c6a8347cc171879 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 6 Jan 2024 11:46:25 -0500 Subject: [PATCH 078/118] Update crate versions for pre-release --- Cargo.toml | 13 +- README.md | 43 ++-- examples/interactive_auth/INTERACTIVE_AUTH.md | 203 ++++-------------- examples/interactive_auth/auth_code.rs | 4 +- examples/interactive_auth/openid.rs | 47 +++- examples/interactive_auth/webview_options.rs | 5 +- graph-core/Cargo.toml | 5 +- graph-error/Cargo.toml | 3 +- graph-http/Cargo.toml | 3 +- graph-oauth/Cargo.toml | 5 +- graph-oauth/README.md | 34 +-- .../credentials/open_id_credential.rs | 1 - 12 files changed, 162 insertions(+), 204 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b567b3d6..79dfcb02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "graph-rs-sdk" -version = "1.1.3" +version = "2.0.0-beta.0" authors = ["sreeise"] edition = "2021" readme = "README.md" license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" -description = "Rust SDK Client for Microsoft Graph and the Microsoft Graph Api" +description = "Rust SDK Client for Microsoft Graph and Microsoft Identity Platform" +homepage = "https://github.com/sreeise/graph-rs-sdk" exclude = [ "test_files/*", @@ -36,10 +37,10 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" -graph-oauth = { path = "./graph-oauth", version = "1.0.3", default-features=false } -graph-http = { path = "./graph-http", version = "1.1.2", default-features=false } -graph-error = { path = "./graph-error", version = "0.2.2" } -graph-core = { path = "./graph-core", version = "0.4.1", default-features=false } +graph-oauth = { path = "./graph-oauth", version = "2.0.0-beta.0", default-features=false } +graph-http = { path = "./graph-http", version = "2.0.0-beta.0", default-features=false } +graph-error = { path = "./graph-error", version = "0.3.0-beta.0" } +graph-core = { path = "./graph-core", version = "2.0.0-beta.0", default-features=false } # When updating or adding new features to this or dependent crates run # cargo tree -e features -i graph-rs-sdk diff --git a/README.md b/README.md index aa397669..9e6f5bc2 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,23 @@ [](https://crates.io/crates/graph-rs-sdk)  -### Rust SDK Client for Microsoft Graph and the Microsoft Graph Api +### Rust SDK Client for Microsoft Graph and Microsoft Identity Platform (Microsoft Entra and personal account sign in) ### Available on [crates.io](https://crates.io/crates/graph-rs-sdk) Features: -- Microsoft Graph V1 and Beta API Client - - Paging using Streaming, Channels, or Iterators. +- Microsoft Graph V1 and Beta API Client + - Paging using Streaming, Channels, or Iterators - Upload Sessions, OData Queries, and File Downloads - Microsoft Graph Identity Platform OAuth2 and OpenId Connect Client - - Auth Code, Client Credentials, Device Code, OpenId, X509 Certificates, PKCE + - Auth Code, Client Credentials, Device Code, OpenId + - X509 Certificates, PKCE - Interactive Authentication - Automatic Token Refresh ```toml -graph-rs-sdk = "1.1.3" +graph-rs-sdk = "2.0.0-beta.0" tokio = { version = "1.25.0", features = ["full"] } ``` @@ -92,7 +93,7 @@ The crate can do both an async and blocking requests. #### Async Client (default) - graph-rs-sdk = "1.1.3" + graph-rs-sdk = "2.0.0-beta.0" tokio = { version = "1.25.0", features = ["full"] } #### Example @@ -124,7 +125,7 @@ async fn main() -> GraphResult<()> { To use the blocking client use the `into_blocking()` method. You should not use `tokio` when using the blocking client. - graph-rs-sdk = "1.1.3" + graph-rs-sdk = "2.0.0-beta.0" #### Example use graph_rs_sdk::*; @@ -1004,6 +1005,17 @@ Support for: - Device Code Polling - Authorization Using Certificates | features = [`openssl`] +#### Detailed Examples: + + +- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth) + - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/auth_code_grant) + - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/openid)) + - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/client_credentials)) +- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/authorization_sign_in) +- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth)) +- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/certificate_auth) + There are two main types for building your chosen OAuth or OpenId Connect Flow. - `PublicClientApplication` @@ -1066,7 +1078,7 @@ async fn build_client( authorization_code: &str, client_id: &str, client_secret: &str, - redirect_uri: &str, + redirect_uri: url::Url, scope: Vec<&str> ) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) @@ -1074,7 +1086,6 @@ async fn build_client( .with_client_secret(client_secret) .with_scope(scope) .with_redirect_uri(redirect_uri) - .unwrap() .build(); let graph_client = Graph::from(confidential_client); @@ -1099,7 +1110,7 @@ lazy_static! { static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap(); } -fn authorization_sign_in_url(client_id: &str, redirect_uri: &str, scope: Vec<String>) -> anyhow::Result<Url> { +fn authorization_sign_in_url(client_id: &str, redirect_uri: url::Url, scope: Vec<String>) -> anyhow::Result<Url> { Ok(AuthorizationCodeCredential::authorization_url_builder(client_id) .with_scope(scope) .with_redirect_uri(redirect_uri) @@ -1111,14 +1122,14 @@ fn build_confidential_client( authorization_code: &str, client_id: &str, client_secret: &str, - redirect_uri: &str, + redirect_uri: url::Url, scope: Vec<String>, ) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> { Ok(ConfidentialClientApplication::builder(client_id) .with_auth_code(authorization_code) .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(redirect_uri) .with_pkce(&PKCE) .build()) } @@ -1208,7 +1219,7 @@ The example below uses the auth code grant. First create the url where the user will sign in. After sign in the user will be redirected back to your app and the authentication code will be in the query of the uri. ```rust -pub fn authorization_sign_in_url(client_id: &str, tenant: &str, redirect_uri: &str) -> Url { +pub fn authorization_sign_in_url(client_id: &str, tenant: &str, redirect_uri: url::Url) -> Url { let scope = vec!["offline_access"]; AuthorizationCodeCredential::authorization_url_builder(client_id) @@ -1227,13 +1238,13 @@ async fn build_client( client_id: &str, client_secret: &str, scope: Vec<String>, // with offline_access - redirect_uri: &str, + redirect_uri: url::Url, ) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential .with_client_secret(client_secret) .with_scope(scope) - .with_redirect_uri(redirect_uri)? + .with_redirect_uri(redirect_uri) .build(); let graph_client = GraphClient::from(&confidential_client); @@ -1262,7 +1273,7 @@ async fn authenticate( tenant_id: &str, client_id: &str, client_secret: &str, - redirect_uri: &str, + redirect_uri: url::Url, scope: Vec<&str>, ) -> anyhow::Result<GraphClient> { std::env::set_var("RUST_LOG", "debug"); diff --git a/examples/interactive_auth/INTERACTIVE_AUTH.md b/examples/interactive_auth/INTERACTIVE_AUTH.md index 8c9e459c..a72d15bf 100644 --- a/examples/interactive_auth/INTERACTIVE_AUTH.md +++ b/examples/interactive_auth/INTERACTIVE_AUTH.md @@ -32,14 +32,14 @@ Requires `features = ["interactive-auth"]` `CredentialBuilder` returned is `AuthorizationCodeCredentialBuilder` ```rust -async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { +async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<AuthorizationCodeCredentialBuilder> { let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri)? - .with_interactive_auth(Default::default())? - .into_result()?; + .with_redirect_uri(redirect_uri) + .with_interactive_auth(Secret(client_secret.to_string()), Default::default()) + .into_credential_builder()?; println!("{authorization_response:#?}"); @@ -54,36 +54,14 @@ Requires `features = ["interactive-auth", "openssl"]` `CredentialBuilder` returned is `AuthorizationCodeCertificateCredentialBuilder` ```rust -async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { +async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<AuthorizationCodeCertificateCredentialBuilder> { let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri)? - .with_certificate_interactive_auth(Default::default(), x509)? - .into_result()?; - - println!("{authorization_response:#?}"); - - Ok(credential_builder) -} -``` - -- Assertion - -Requires `features = ["interactive-auth"]` - -`CredentialBuilder` returned is `AuthorizationCodeAssertionCredentialBuilder` - -```rust -async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<AuthorizationCodeAssertionCredentialBuilder> { - let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(vec!["user.read"]) - .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri)? - .with_assertion_interactive_auth(Default::default())? - .into_result()?; + .with_redirect_uri(redirect_uri) + .with_certificate_interactive_auth(x509, Default::default()) + .into_credential_builder()?; println!("{authorization_response:#?}"); @@ -93,10 +71,11 @@ async fn authenticate(x509: &X509Certificate, tenant_id: &str, client_id: &str, ### Convenience Methods -The `into_result` method transforms the `AuthorizationEvent` that is normally returned from -`with_interactive_authentication` into `(AuthorizationResponse, CredentialBuilder)`. +The `into_credential_builder` method maps the `WebViewAuthorizationEvent` and `Result<WebViewAuthorizationEvent>` +that is normally returned from `with_interactive_auth` into `(AuthorizationResponse, CredentialBuilder)` +and `Result<(AuthorizationResponse, CredentialBuilder)>` respectively. -By default `with_interactive_authentication` returns `AuthorizationEvent<CredentialBuilder>` which can provide +By default `with_interactive_auth` returns `AuthorizationEvent<CredentialBuilder>` which can provide the caller with useful information about the events happening with the webview such as if the user closed the window. For those that don't necessarily care about those events use `into_result` to transform the `AuthorizationEvent` @@ -105,14 +84,14 @@ into the credential builder that can be built and passed to the `GraphClient`. See [Reacting To Events](#reacting-to-events) to learn more. ```rust -async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> anyhow::Result<()> { +async fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, redirect_uri: url::Url) -> anyhow::Result<()> { let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(vec!["user.read"]) .with_offline_access() // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri)? - .with_interactive_authentication(Default::default())? - .into_result()?; + .with_redirect_uri(redirect_uri) + .with_interactive_auth(Secret(client_secret.to_string()), Default::default()) + .into_credential_builder()?; Ok(()) } ``` @@ -123,147 +102,59 @@ You can customize several aspects of the webview and how the webview is used to using `WebViewOptions`. ```rust -use graph_rs_sdk::oauth::{web::Theme, web::WebViewOptions, AuthorizationCodeCredential}; +use graph_rs_sdk::identity::{ + interactive::Theme, interactive::WebViewOptions, interactive::WithInteractiveAuth, + AuthorizationCodeCredential, IntoCredentialBuilder, Secret, +}; +use graph_rs_sdk::GraphClient; +use std::collections::HashSet; use std::ops::Add; use std::time::{Duration, Instant}; +use url::Url; fn get_webview_options() -> WebViewOptions { WebViewOptions::builder() // Give the window a title. The default is "Sign In" - .with_window_title("Sign In") - // OS specific theme. Windows Only. + .window_title("Sign In") + // OS specific theme. Windows only. // See wry crate for more info. - .with_theme(Theme::Dark) + .theme(Theme::Dark) // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the // user to log in and the user has not logged in after 20 minutes you may // want to assume the user is idle in some way and close out of the webview window. - .with_timeout(Instant::now().add(Duration::from_secs(1200))) + .timeout(Instant::now().add(Duration::from_secs(1200))) // The webview can store the cookies that were set after sign in so that on the next // sign in the user is automatically logged in through SSO. Or you can clear the browsing // data, cookies in this case, after sign in when the webview window closes. - .with_clear_browsing_data(false) + // Default is false. + // When using webview and the user is automatically logged in the webview + // will only show temporarily and then close itself. + .clear_browsing_data_on_close(true) // Provide a list of ports to use for interactive authentication. // This assumes that you have http://localhost or http://localhost:port // for each port registered in your ADF application registration. - .with_ports(&[8000]) + .ports(HashSet::from([8000])) } -async fn customize_webview(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<()> { - let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(scope) - .with_redirect_uri(redirect_uri)? - .with_interactive_authentication(get_webview_options())? - .into_result()?; - - Ok(()) -} -``` - -### Reacting To Events - -By default `with_interactive_authentication` returns `AuthorizationEvent<CredentialBuilder>` which can provide -the caller with useful information about the events happening with the webview such as if the user closed the window. - -For those that don't necessarily care about those events use `into_result` to transform the `AuthorizationEvent` -into the credential builder that can be built and passed to the `GraphClient`. - -```rust -fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<GraphClient> { - let authorization_event = - OpenIdCredential::authorization_url_builder(client_id) +async fn customize_webview( + tenant_id: &str, + client_id: &str, + client_secret: &str, + scope: Vec<&str>, + redirect_uri: &str, +) -> anyhow::Result<GraphClient> { + let (authorization_response, mut credential_builder) = + AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) - .with_scope(vec!["user.read", "offline_access", "email", "profile"]) - .with_response_mode(ResponseMode::Fragment) - .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_redirect_uri(redirect_uri)? - .with_interactive_auth_for_secret(Default::default())?; - - match authorization_event { - AuthorizationEvent::Authorized { authorization_response, mut credential_builder } => { - println!("{authorization_response:#?}"); - - let mut confidential_client = credential_builder - .with_client_secret(client_secret) - .build(); - - Ok(GraphClient::from(&confidential_client)) - } - AuthorizationEvent::Unauthorized(authorization_response) => { - println!("{authorization_response:#?}"); - Err(anyhow!(format!("error: {:#?}, error_description: {:#?}, error_uri: {:#?}", authorization_response.error, authorization_response.error_description, authorization_response.error_uri))) - } - AuthorizationEvent::Impeded(AuthorizationImpeded::WindowClosed(reason)) => { - println!("Authorization Impeded With Reason: {reason:#?}"); - Err(anyhow!(reason)) - } - AuthorizationEvent::Impeded(AuthorizationImpeded::InvalidUri(reason)) => { - println!("Authorization Impeded With Reason: {reason:#?}"); - Err(anyhow!(reason)) - } - } -} -``` - -The `Unauthorized` and `Impeded` variants of `AuthorizationImpeded` useful for error handling inside an application. - -1. `AuthorizationImpeded::WindowClosed(reason: String)` - Where reason is one of: - - CloseRequested: The user closed the window before finishing login. - - TimedOut: A timeout was reached that you can set in `WebViewOptions`. Suppose your waiting on the user to sign - in but the window has been idle (no user interaction) for 20 minutes. You can specify a timeout such as 20 minutes - to close the window and perform some other work. - - WindowDestroyed: Either the window was destroyed or the webview event loop was destroyed causing the window - to also be destroyed. The cause is unknown. The login did not finish. -2. `AuthorizationImpeded::InvalidUri(reason)` - - Where reason is a message for why the URI is invalid and the URI itself. + .with_scope(scope) + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth(Secret(client_secret.to_string()), get_webview_options()) + .into_credential_builder()?; + let confidential_client = credential_builder.build(); -The third variant `Authorized` means that the query or fragment of the URL was successfully parsed -from a redirect after sign in. This variant provides the parsed `AuthorizationResponse` from the -query or fragment and the `CredentialBuilder` that you can build and pass to the `GraphClient`: - -```rust -fn authenticate(tenant_id: &str, client_id: &str, client_secret: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<GraphClient> { - let authorization_event = - OpenIdCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(vec!["user.read", "offline_access", "email", "profile"]) - .with_response_mode(ResponseMode::Fragment) - .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) - .with_redirect_uri(redirect_uri)? - .with_interactive_auth_for_secret(Default::default())?; - - match authorization_event { - AuthorizationEvent::Authorized { authorization_response, mut credential_builder } => { - println!("{authorization_response:#?}"); - - let mut confidential_client = credential_builder - .with_client_secret(client_secret) - .build(); - - Ok(GraphClient::from(&confidential_client)) - } - _ => Err(anyhow!("failed")) - } + Ok(GraphClient::from(&confidential_client)) } -``` - -Using `map_to_credential_builder` transforms the `Unauthorized` and `Impeded` variants of `AuthorizationEvent` -into `WebViewError` which is then returned in the result `Result<(AuthorizationResponse, CredentialBuilder), WebViewError>` -```rust -use graph_rs_sdk::identity::{AuthorizationCodeCredential, Secret, WithInteractiveAuth}; - -async fn authenticate(tenant_id: &str, client_id: &str, scope: Vec<&str>, redirect_uri: &str) -> anyhow::Result<()> { - let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) - .with_tenant(tenant_id) - .with_scope(scope) - .with_redirect_uri(redirect_uri)? - .with_interactive_auth(Secret("client-secret".to_string()), Default::default()) - .into_result()?; - - Ok(()) -} ``` diff --git a/examples/interactive_auth/auth_code.rs b/examples/interactive_auth/auth_code.rs index 5a80618b..f7288566 100644 --- a/examples/interactive_auth/auth_code.rs +++ b/examples/interactive_auth/auth_code.rs @@ -36,7 +36,7 @@ async fn authenticate( std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); - let (authorization_query_response, credential_builder) = + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. @@ -44,7 +44,7 @@ async fn authenticate( .with_interactive_auth(Secret("secret".to_string()), Default::default()) .into_credential_builder()?; - debug!("{authorization_query_response:#?}"); + debug!("{authorization_response:#?}"); let confidential_client = credential_builder.build(); diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index 430a98a0..1f430533 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -1,5 +1,8 @@ +use anyhow::anyhow; use graph_rs_sdk::{ http::Url, + identity::interactive::WebViewAuthorizationEvent, + identity::OpenIdCredentialBuilder, identity::{IntoCredentialBuilder, OpenIdCredential, ResponseMode, ResponseType}, GraphClient, }; @@ -13,7 +16,7 @@ async fn openid_authenticate( std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); - let (authorization_query_response, mut credential_builder) = + let (authorization_response, mut credential_builder) = OpenIdCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(vec!["user.read", "offline_access", "profile", "email"]) // Adds offline_access as a scope which is needed to get a refresh token. @@ -23,9 +26,49 @@ async fn openid_authenticate( .with_interactive_auth(client_secret, Default::default()) .into_credential_builder()?; - debug!("{authorization_query_response:#?}"); + debug!("{authorization_response:#?}"); let confidential_client = credential_builder.build(); Ok(GraphClient::from(&confidential_client)) } + +async fn openid_authenticate2( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: &str, +) -> anyhow::Result<GraphClient> { + std::env::set_var("RUST_LOG", "debug"); + pretty_env_logger::init(); + + let auth_event = OpenIdCredential::authorization_url_builder(client_id) + .with_tenant(tenant_id) + .with_scope(vec!["user.read", "offline_access", "profile", "email"]) // Adds offline_access as a scope which is needed to get a refresh token. + .with_response_mode(ResponseMode::Fragment) + .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth(client_secret, Default::default())?; + + match auth_event { + WebViewAuthorizationEvent::Authorized { + authorization_response, + credential_builder, + } => { + debug!("{authorization_response:#?}"); + let confidential_client = credential_builder.build(); + Ok(GraphClient::from(&confidential_client)) + } + WebViewAuthorizationEvent::Unauthorized(authorization_response) => { + debug!("{authorization_response:#?}"); + Err(anyhow!("error signing in")) + } + WebViewAuthorizationEvent::WindowClosed(reason) => { + // The webview window closed before sign in was complete. The can happen due to various issues + // but most likely it was either that the user closed the window before finishing sign in or + // there was an error returned from Microsoft Graph. + println!("{:#?}", reason); + Err(anyhow!(reason)) + } + } +} diff --git a/examples/interactive_auth/webview_options.rs b/examples/interactive_auth/webview_options.rs index dd9b1668..1a3f1fd5 100644 --- a/examples/interactive_auth/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -24,7 +24,10 @@ fn get_webview_options() -> WebViewOptions { // The webview can store the cookies that were set after sign in so that on the next // sign in the user is automatically logged in through SSO. Or you can clear the browsing // data, cookies in this case, after sign in when the webview window closes. - .clear_browsing_data_on_close(false) + // Default is false. + // When using webview and the user is automatically logged in the webview + // will only show temporarily and then close itself. + .clear_browsing_data_on_close(true) // Provide a list of ports to use for interactive authentication. // This assumes that you have http://localhost or http://localhost:port // for each port registered in your ADF application registration. diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index bfd4d989..e3a4ce21 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "graph-core" -version = "0.4.1" +version = "2.0.0-beta.0" authors = ["sreeise"] edition = "2021" license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" description = "Common types and traits for the graph-rs-sdk crate" +homepage = "https://github.com/sreeise/graph-rs-sdk" [dependencies] async-stream = "0.3" @@ -26,7 +27,7 @@ remain = "0.2.6" tracing = "0.1.37" url = { version = "2", features = ["serde"] } -graph-error = { path = "../graph-error" } +graph-error = { version = "0.3.0-beta.0", path = "../graph-error" } [features] default = ["native-tls"] diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 1070d5b6..7728b45f 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "graph-error" -version = "0.2.2" +version = "0.3.0-beta.0" authors = ["sreeise"] edition = "2021" license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" description = "Graph Api error types and handling for the graph-rs-sdk crate" +homepage = "https://github.com/sreeise/graph-rs-sdk" keywords = ["onedrive", "microsoft", "microsoft-graph", "api", "oauth"] categories = ["authentication", "web-programming::http-client"] diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 211fb45d..7fe45086 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -1,11 +1,12 @@ [package] name = "graph-http" -version = "1.1.2" +version = "2.0.0-beta.0" authors = ["sreeise"] edition = "2021" license = "MIT" repository = "https://github.com/sreeise/graph-rs-sdk" description = "Http client and utilities for the graph-rs-sdk crate" +homepage = "https://github.com/sreeise/graph-rs-sdk" [dependencies] async-stream = "0.3" diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 6050d436..cb34294b 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "graph-oauth" -version = "1.0.3" +version = "2.0.0-beta.0" authors = ["sreeise"] edition = "2021" license = "MIT" readme = "README.md" repository = "https://github.com/sreeise/graph-rs-sdk" -description = "OAuth client implementing the OAuth 2.0 and OpenID Connect protocols for Microsoft Identity Platform" +description = "Rust SDK Client for Microsoft Identity Platform" +homepage = "https://github.com/sreeise/graph-rs-sdk" keywords = ["microsoft", "oauth", "authentication", "authorization"] categories = ["authentication", "web-programming::http-client"] diff --git a/graph-oauth/README.md b/graph-oauth/README.md index 74060dd0..7689f898 100644 --- a/graph-oauth/README.md +++ b/graph-oauth/README.md @@ -1,4 +1,4 @@ -# OAuth 2.0 and OpenID Connect Client For The Microsoft Identity Platform +# Rust SDK Client For The Microsoft Identity Platform Support for: @@ -28,14 +28,14 @@ for Microsoft Identity Platform or by using [graph-rs-sdk](https://crates.io/cra For async: ```toml -graph-oauth = "1.0.2" +graph-oauth = "2.0.0-beta.0" tokio = { version = "1.25.0", features = ["full"] } ``` For blocking: ```toml -graph-oauth = "1.0.2" +graph-oauth = "2.0.0-beta.0" ``` ### Feature Flags @@ -108,7 +108,7 @@ async fn build_client( authorization_code: &str, client_id: &str, client_secret: &str, - redirect_uri: &str, + redirect_uri: url::Url, scope: Vec<&str> ) -> anyhow::Result<GraphClient> { let mut confidential_client = ConfidentialClientApplication::builder(client_id) @@ -118,7 +118,7 @@ async fn build_client( .with_redirect_uri(redirect_uri)? .build(); - let graph_client = Graph::from(confidential_client); + let graph_client = Graph::from(&confidential_client); Ok(graph_client) } @@ -197,7 +197,7 @@ Tokens will still be automatically refreshed as this flow does not require using a new access token. ```rust -async fn authenticate(client_id: &str, tenant: &str, redirect_uri: &str) { +async fn authenticate(client_id: &str, tenant: &str, redirect_uri: url::Url) { let scope = vec!["offline_access"]; let mut credential_builder = ConfidentialClientApplication::builder(client_id) @@ -223,7 +223,14 @@ Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) cra platforms that support it such as on a desktop. ```rust -use graph_rs_sdk::{oauth::AuthorizationCodeCredential, GraphClient}; +use graph_rs_sdk::{ + identity::{ + interactive::WithInteractiveAuth, AuthorizationCodeCredential, IntoCredentialBuilder, + Secret, + }, + GraphClient, + http::Url, +}; async fn authenticate( tenant_id: &str, @@ -235,19 +242,18 @@ async fn authenticate( std::env::set_var("RUST_LOG", "debug"); pretty_env_logger::init(); - let (authorization_query_response, mut credential_builder) = + let (authorization_response, credential_builder) = AuthorizationCodeCredential::authorization_url_builder(client_id) .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. - .with_redirect_uri(redirect_uri) - .with_interactive_authentication_for_secret(Default::default()) - .unwrap(); + .with_redirect_uri(Url::parse(redirect_uri)?) + .with_interactive_auth(Secret("secret".to_string()), Default::default()) + .into_credential_builder()?; - debug!("{authorization_query_response:#?}"); + debug!("{authorization_response:#?}"); - let mut confidential_client = credential_builder.with_client_secret(client_secret).build(); + let confidential_client = credential_builder.build(); Ok(GraphClient::from(&confidential_client)) } - ``` diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index ac50979f..94b116b7 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -723,7 +723,6 @@ impl OpenIdCredentialBuilder { self } - /// Defaults to http://localhost pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); Ok(self) From 673f1a7a30d357cc96dfb6605aeb0eb9248baff5 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:35:36 -0500 Subject: [PATCH 079/118] Add crate for webview in separate execution --- Cargo.toml | 1 + README.md | 19 ++++++++++--------- graph-oauth/Cargo.toml | 2 -- graph-rs-interactive-auth/Cargo.toml | 16 ++++++++++++++++ graph-rs-interactive-auth/src/main.rs | 3 +++ test-tools/src/oauth_request.rs | 2 +- 6 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 graph-rs-interactive-auth/Cargo.toml create mode 100644 graph-rs-interactive-auth/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 79dfcb02..2de7f1a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ members = [ "graph-codegen", "graph-http", "graph-core", + "graph-rs-interactive-auth", ] [dependencies] diff --git a/README.md b/README.md index 9e6f5bc2..92be64e9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # graph-rs-sdk -[](https://crates.io/crates/graph-rs-sdk)  -### Rust SDK Client for Microsoft Graph and Microsoft Identity Platform (Microsoft Entra and personal account sign in) + + +### Rust SDK Client for Microsoft Graph and Microsoft Identity Platform ### Available on [crates.io](https://crates.io/crates/graph-rs-sdk) @@ -1008,13 +1009,13 @@ Support for: #### Detailed Examples: -- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth) - - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/auth_code_grant) - - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/openid)) - - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth/client_credentials)) -- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/authorization_sign_in) -- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth)) -- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/certificate_auth) +- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) + - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) + - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) + - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/client_credentials)) +- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/authorization_sign_in) +- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth)) +- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/certificate_auth) There are two main types for building your chosen OAuth or OpenId Connect Flow. diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index cb34294b..ae84dfe0 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -34,12 +34,10 @@ serde_urlencoded = "0.7.1" strum = { version = "0.25.0", features = ["derive"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } -webbrowser = "0.8.7" wry = { version = "0.33.1", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" -tracing-futures = "0.2.5" graph-error = { path = "../graph-error" } graph-core = { path = "../graph-core", default-features = false } diff --git a/graph-rs-interactive-auth/Cargo.toml b/graph-rs-interactive-auth/Cargo.toml new file mode 100644 index 00000000..3baa6195 --- /dev/null +++ b/graph-rs-interactive-auth/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "graph-rs-interactive-auth" +version = "0.1.0" +edition = "2021" +license = "MIT" +repository = "https://github.com/sreeise/graph-rs-sdk" +description = "Interactive Auth For The Microsoft Identity Platform" +homepage = "https://github.com/sreeise/graph-rs-sdk" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = { version = "1.0.69", features = ["backtrace"]} +wry = { version = "0.33.1" } +tracing = "0.1.37" +url = { version = "2", features = ["serde"] } diff --git a/graph-rs-interactive-auth/src/main.rs b/graph-rs-interactive-auth/src/main.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/graph-rs-interactive-auth/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/test-tools/src/oauth_request.rs b/test-tools/src/oauth_request.rs index 0cd1ada4..ca1e02e7 100644 --- a/test-tools/src/oauth_request.rs +++ b/test-tools/src/oauth_request.rs @@ -328,7 +328,7 @@ impl OAuthTestClient { let (test_client, credentials) = client.default_client()?; if let Some((id, token)) = test_client.get_access_token(credentials) { - Some((id, Graph::new(token.access_token))) + Some((id, GraphClient::new(token.access_token))) } else { None } From 2d58678b5eeaeafeedffb92f2d22b831aef5b789 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:51:25 -0500 Subject: [PATCH 080/118] Fix changes that occured in merging master --- Cargo.toml | 2 +- graph-rs-interactive-auth/Cargo.toml | 16 ---------------- graph-rs-interactive-auth/src/main.rs | 3 --- src/drives/workbook_functions/request.rs | 2 +- src/drives/workbook_tables/request.rs | 2 +- src/drives/workbook_tables_columns/request.rs | 2 +- src/drives/workbook_tables_rows/request.rs | 2 +- src/drives/worksheets_charts/request.rs | 2 +- src/drives/worksheets_charts_axes/request.rs | 2 +- .../request.rs | 2 +- .../request.rs | 2 +- .../worksheets_charts_axes_value_axis/request.rs | 2 +- .../worksheets_charts_data_labels/request.rs | 2 +- src/drives/worksheets_charts_format/request.rs | 2 +- src/drives/worksheets_charts_legend/request.rs | 2 +- src/drives/worksheets_charts_series/request.rs | 2 +- src/drives/worksheets_charts_title/request.rs | 2 +- 17 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 graph-rs-interactive-auth/Cargo.toml delete mode 100644 graph-rs-interactive-auth/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2de7f1a3..f925218d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "graph-codegen", "graph-http", "graph-core", - "graph-rs-interactive-auth", + "graph-rs-sdk-interactive-auth" ] [dependencies] diff --git a/graph-rs-interactive-auth/Cargo.toml b/graph-rs-interactive-auth/Cargo.toml deleted file mode 100644 index 3baa6195..00000000 --- a/graph-rs-interactive-auth/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "graph-rs-interactive-auth" -version = "0.1.0" -edition = "2021" -license = "MIT" -repository = "https://github.com/sreeise/graph-rs-sdk" -description = "Interactive Auth For The Microsoft Identity Platform" -homepage = "https://github.com/sreeise/graph-rs-sdk" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -anyhow = { version = "1.0.69", features = ["backtrace"]} -wry = { version = "0.33.1" } -tracing = "0.1.37" -url = { version = "2", features = ["serde"] } diff --git a/graph-rs-interactive-auth/src/main.rs b/graph-rs-interactive-auth/src/main.rs deleted file mode 100644 index e7a11a96..00000000 --- a/graph-rs-interactive-auth/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/src/drives/workbook_functions/request.rs b/src/drives/workbook_functions/request.rs index aba816a0..e18ad7c8 100644 --- a/src/drives/workbook_functions/request.rs +++ b/src/drives/workbook_functions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorkbookFunctionsApiClient, ResourceIdentity::WorkbookFunctions ); diff --git a/src/drives/workbook_tables/request.rs b/src/drives/workbook_tables/request.rs index e62a0426..b6ad8a3d 100644 --- a/src/drives/workbook_tables/request.rs +++ b/src/drives/workbook_tables/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorkbookTablesApiClient, WorkbookTablesIdApiClient, ResourceIdentity::WorkbookTables diff --git a/src/drives/workbook_tables_columns/request.rs b/src/drives/workbook_tables_columns/request.rs index ce3fefc2..d350accd 100644 --- a/src/drives/workbook_tables_columns/request.rs +++ b/src/drives/workbook_tables_columns/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorkbookTablesColumnsApiClient, WorkbookTablesColumnsIdApiClient, ResourceIdentity::WorkbookTablesColumns diff --git a/src/drives/workbook_tables_rows/request.rs b/src/drives/workbook_tables_rows/request.rs index c84b33d2..c9b25287 100644 --- a/src/drives/workbook_tables_rows/request.rs +++ b/src/drives/workbook_tables_rows/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorkbookTablesRowsApiClient, WorkbookTablesRowsIdApiClient, ResourceIdentity::WorkbookTablesRows diff --git a/src/drives/worksheets_charts/request.rs b/src/drives/worksheets_charts/request.rs index 64b1050e..356987d3 100644 --- a/src/drives/worksheets_charts/request.rs +++ b/src/drives/worksheets_charts/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsApiClient, WorksheetsChartsIdApiClient, ResourceIdentity::WorksheetsCharts diff --git a/src/drives/worksheets_charts_axes/request.rs b/src/drives/worksheets_charts_axes/request.rs index 5d591e6b..cca9b78e 100644 --- a/src/drives/worksheets_charts_axes/request.rs +++ b/src/drives/worksheets_charts_axes/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsAxesApiClient, ResourceIdentity::WorksheetsChartsAxes ); diff --git a/src/drives/worksheets_charts_axes_category_axis/request.rs b/src/drives/worksheets_charts_axes_category_axis/request.rs index 45eb6cf3..a4d2b127 100644 --- a/src/drives/worksheets_charts_axes_category_axis/request.rs +++ b/src/drives/worksheets_charts_axes_category_axis/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsAxesCategoryAxisApiClient, ResourceIdentity::WorksheetsChartsAxesCategoryAxis ); diff --git a/src/drives/worksheets_charts_axes_series_axis/request.rs b/src/drives/worksheets_charts_axes_series_axis/request.rs index 5718a7e4..88d701a4 100644 --- a/src/drives/worksheets_charts_axes_series_axis/request.rs +++ b/src/drives/worksheets_charts_axes_series_axis/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsAxesSeriesAxisApiClient, ResourceIdentity::WorksheetsChartsAxesSeriesAxis ); diff --git a/src/drives/worksheets_charts_axes_value_axis/request.rs b/src/drives/worksheets_charts_axes_value_axis/request.rs index 343fba70..163807a0 100644 --- a/src/drives/worksheets_charts_axes_value_axis/request.rs +++ b/src/drives/worksheets_charts_axes_value_axis/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsAxesValueAxisApiClient, ResourceIdentity::WorksheetsChartsAxesValueAxis ); diff --git a/src/drives/worksheets_charts_data_labels/request.rs b/src/drives/worksheets_charts_data_labels/request.rs index 055e76b6..cc0efe68 100644 --- a/src/drives/worksheets_charts_data_labels/request.rs +++ b/src/drives/worksheets_charts_data_labels/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsDataLabelsApiClient, ResourceIdentity::WorksheetsChartsDataLabels ); diff --git a/src/drives/worksheets_charts_format/request.rs b/src/drives/worksheets_charts_format/request.rs index 7e78be53..758ab092 100644 --- a/src/drives/worksheets_charts_format/request.rs +++ b/src/drives/worksheets_charts_format/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorksheetsChartsFormatApiClient, ResourceIdentity::WorksheetsChartsFormat ); diff --git a/src/drives/worksheets_charts_legend/request.rs b/src/drives/worksheets_charts_legend/request.rs index 0827bdf7..4ca32a6a 100644 --- a/src/drives/worksheets_charts_legend/request.rs +++ b/src/drives/worksheets_charts_legend/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorksheetsChartsLegendApiClient, ResourceIdentity::WorksheetsChartsLegend ); diff --git a/src/drives/worksheets_charts_series/request.rs b/src/drives/worksheets_charts_series/request.rs index 14d23ff1..fbef7b39 100644 --- a/src/drives/worksheets_charts_series/request.rs +++ b/src/drives/worksheets_charts_series/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( WorksheetsChartsSeriesApiClient, WorksheetsChartsSeriesIdApiClient, ResourceIdentity::WorksheetsChartsSeries diff --git a/src/drives/worksheets_charts_title/request.rs b/src/drives/worksheets_charts_title/request.rs index 56508923..7b66357c 100644 --- a/src/drives/worksheets_charts_title/request.rs +++ b/src/drives/worksheets_charts_title/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::drives::*; -resource_api_client!( +api_client!( WorksheetsChartsTitleApiClient, ResourceIdentity::WorksheetsChartsTitle ); From c2e692b765956b00f10ec13cff1703f7aeac5340 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sun, 28 Jan 2024 05:22:50 -0500 Subject: [PATCH 081/118] Update wry crate and add tao crate based on changes in wry --- Cargo.toml | 2 +- graph-oauth/Cargo.toml | 3 +- .../src/interactive/interactive_auth.rs | 12 +++--- .../src/interactive/webview_options.rs | 5 +-- graph-rs-sdk-webview/Cargo.toml | 15 +++++++ graph-rs-sdk-webview/src/main.rs | 40 +++++++++++++++++++ 6 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 graph-rs-sdk-webview/Cargo.toml create mode 100644 graph-rs-sdk-webview/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index f925218d..715cd2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ members = [ "graph-codegen", "graph-http", "graph-core", - "graph-rs-sdk-interactive-auth" + "graph-rs-sdk-webview" ] [dependencies] diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index ae84dfe0..14424437 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -32,9 +32,10 @@ serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.25.0", features = ["derive"] } +tao = { version = "0.24.1", features = ["serde"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } -wry = { version = "0.33.1", optional = true } +wry = { version = "0.35.2", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" diff --git a/graph-oauth/src/interactive/interactive_auth.rs b/graph-oauth/src/interactive/interactive_auth.rs index af25c7fb..c98fabf3 100644 --- a/graph-oauth/src/interactive/interactive_auth.rs +++ b/graph-oauth/src/interactive/interactive_auth.rs @@ -3,17 +3,17 @@ use crate::interactive::{HostOptions, WebViewOptions}; use std::fmt::{Debug, Display, Formatter}; use std::sync::mpsc::Sender; use std::time::{Duration, Instant}; +use tao::event::{Event, StartCause, WindowEvent}; +use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; +use tao::window::{Window, WindowBuilder}; use url::Url; -use wry::application::event::{Event, StartCause, WindowEvent}; -use wry::application::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; -use wry::application::window::{Window, WindowBuilder}; -use wry::webview::WebView; +use wry::WebView; #[cfg(target_family = "unix")] -use wry::application::platform::unix::EventLoopBuilderExtUnix; +use tao::platform::unix::EventLoopBuilderExtUnix; #[cfg(target_family = "windows")] -use wry::application::platform::windows::EventLoopBuilderExtWindows; +use tao::platform::windows::EventLoopBuilderExtWindows; #[derive(Clone, Debug)] pub enum WindowCloseReason { diff --git a/graph-oauth/src/interactive/webview_options.rs b/graph-oauth/src/interactive/webview_options.rs index c2481ff8..c6642712 100644 --- a/graph-oauth/src/interactive/webview_options.rs +++ b/graph-oauth/src/interactive/webview_options.rs @@ -1,9 +1,8 @@ use std::collections::HashSet; use std::time::Instant; +use tao::window::Theme; use url::Url; -pub use wry::application::window::Theme; - #[derive(Clone, Debug)] pub struct HostOptions { pub(crate) start_uri: Url, @@ -36,7 +35,7 @@ pub struct WebViewOptions { /// Give the window a title. The default is "Sign In" pub window_title: String, /// OS specific theme. Only available on Windows. - /// See wry crate for more info. + /// See tao/wry crate for more info. /// /// Theme is not set by default. #[cfg(windows)] diff --git a/graph-rs-sdk-webview/Cargo.toml b/graph-rs-sdk-webview/Cargo.toml new file mode 100644 index 00000000..44f85ef4 --- /dev/null +++ b/graph-rs-sdk-webview/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "graph-rs-sdk-webview" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/sreeise/graph-rs-sdk" +description = "Webview interactive auth for Microsoft identity platform" +homepage = "https://github.com/sreeise/graph-rs-sdk" + +[dependencies] +clap = { version = "4.4.18", features = ["derive"] } +tao = { version = "0.24.1", features = ["serde"] } +time = { version = "0.3.10", features = ["local-offset", "serde"] } +url = { version = "2", features = ["serde"] } +wry = { version = "0.35.2" } + diff --git a/graph-rs-sdk-webview/src/main.rs b/graph-rs-sdk-webview/src/main.rs new file mode 100644 index 00000000..a9f0311d --- /dev/null +++ b/graph-rs-sdk-webview/src/main.rs @@ -0,0 +1,40 @@ +use std::collections::HashSet; +use std::time::Instant; +use clap::Parser; + +// `C:\Users\reeis\src\graph-rs\target\debug\graph-rs-sdk-webview.exe +// cargo run --bin graph-rs-sdk-webview -- --window-title my_window_title + +fn main() { + let args = Args::parse(); + + println!("{:#?}", args.window_title); +} + +#[derive(Debug, Parser)] +#[command(name = "graph-rs-sdk-webview")] +#[command(author, version)] +struct Args { + #[arg(short, long)] + window_title: Option<String>, + + #[arg(short, long)] + ports: Option<String>, + + /// Add a timeout that will close the window and return an error + /// when that timeout is reached. For instance, if your app is waiting on the + /// user to log in and the user has not logged in after 20 minutes you may + /// want to assume the user is idle in some way and close out of the webview window. + /// + /// Default is no timeout. + //#[arg(short, long)] + ///pub timeout: Option<Instant>, + + /// The webview can store the cookies that were set after sign in so that on the next + /// sign in the user is automatically logged in through SSO. Or you can clear the browsing + /// data, cookies in this case, after sign in when the webview window closes. + /// + /// Default is false + #[arg(short, long)] + pub clear_data: bool, +} From fd17ab1b43ffc248eb15201c8f96472490769499 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 1 Feb 2024 01:28:57 -0500 Subject: [PATCH 082/118] Fix missing imports after upgrading version of wry and adding tao --- graph-oauth/src/identity/credentials/as_query.rs | 2 +- .../identity/credentials/auth_code_authorization_url.rs | 8 +++----- .../src/identity/credentials/client_builder_impl.rs | 2 +- .../src/identity/credentials/device_code_credential.rs | 8 +++----- graph-oauth/src/identity/credentials/mod.rs | 2 +- .../src/identity/credentials/open_id_authorization_url.rs | 8 +++----- graph-oauth/src/interactive/interactive_auth.rs | 4 ++-- graph-rs-sdk-webview/src/main.rs | 2 +- 8 files changed, 15 insertions(+), 21 deletions(-) diff --git a/graph-oauth/src/identity/credentials/as_query.rs b/graph-oauth/src/identity/credentials/as_query.rs index dd32c910..eca4cf24 100644 --- a/graph-oauth/src/identity/credentials/as_query.rs +++ b/graph-oauth/src/identity/credentials/as_query.rs @@ -1,4 +1,4 @@ -pub trait AsQuery<RHS = Self> { +pub(crate) trait AsQuery<RHS = Self> { fn as_query(&self) -> String; } diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 8c31f2e2..ec0015f5 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -32,10 +32,8 @@ use { }, crate::{Assertion, Secret}, graph_error::{AuthExecutionError, WebViewError, WebViewResult}, - wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, - }, + tao::{event_loop::EventLoopProxy, window::Window}, + wry::{WebView, WebViewBuilder}, }; credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); @@ -350,7 +348,7 @@ mod internal { impl WebViewAuth for AuthCodeAuthorizationUrlParameters { fn webview( host_options: HostOptions, - window: Window, + window: &Window, proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView> { let start_uri = host_options.start_uri.clone(); diff --git a/graph-oauth/src/identity/credentials/client_builder_impl.rs b/graph-oauth/src/identity/credentials/client_builder_impl.rs index 47bd2f7d..96b23765 100644 --- a/graph-oauth/src/identity/credentials/client_builder_impl.rs +++ b/graph-oauth/src/identity/credentials/client_builder_impl.rs @@ -22,7 +22,7 @@ macro_rules! credential_builder_base { pub fn with_azure_cloud_instance( &mut self, - azure_cloud_instance: AzureCloudInstance, + azure_cloud_instance: crate::identity::AzureCloudInstance, ) -> &mut Self { self.credential .app_config diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 76becef0..c6f64b26 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -30,10 +30,8 @@ use { crate::interactive::{HostOptions, UserEvents, WebViewAuth, WebViewOptions}, crate::tracing_targets::INTERACTIVE_AUTH, graph_error::WebViewDeviceCodeError, - wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, - }, + tao::{event_loop::EventLoopProxy, window::Window}, + wry::{WebView, WebViewBuilder}, }; const DEVICE_CODE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:device_code"; @@ -574,7 +572,7 @@ pub(crate) mod internal { impl WebViewAuth for DeviceCodeCredential { fn webview( host_options: HostOptions, - window: Window, + window: &Window, _proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView> { Ok(WebViewBuilder::new(window)? diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 9cfa1eb4..25267503 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -1,6 +1,6 @@ pub use app_config::*; pub use application_builder::*; -pub use as_query::*; +pub(crate) use as_query::*; pub use auth_code_authorization_url::*; pub use authorization_code_assertion_credential::*; pub use authorization_code_certificate_credential::*; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index f94893fd..a910f8ef 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -26,10 +26,8 @@ use { WebViewHostValidator, WebViewOptions, }, graph_error::{WebViewError, WebViewResult}, - wry::{ - application::{event_loop::EventLoopProxy, window::Window}, - webview::{WebView, WebViewBuilder}, - }, + tao::{event_loop::EventLoopProxy, window::Window}, + wry::{WebView, WebViewBuilder}, }; const RESPONSE_TYPES_SUPPORTED: &[&str] = &["code", "id_token", "code id_token", "id_token token"]; @@ -405,7 +403,7 @@ impl AuthorizationUrl for OpenIdAuthorizationUrlParameters { impl WebViewAuth for OpenIdAuthorizationUrlParameters { fn webview( host_options: HostOptions, - window: Window, + window: &Window, proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView> { let start_uri = host_options.start_uri.clone(); diff --git a/graph-oauth/src/interactive/interactive_auth.rs b/graph-oauth/src/interactive/interactive_auth.rs index c98fabf3..5136eaa4 100644 --- a/graph-oauth/src/interactive/interactive_auth.rs +++ b/graph-oauth/src/interactive/interactive_auth.rs @@ -55,7 +55,7 @@ where { fn webview( host_options: HostOptions, - window: Window, + window: &Window, proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView>; @@ -69,7 +69,7 @@ where let proxy = event_loop.create_proxy(); let window = Self::window_builder(&options).build(&event_loop).unwrap(); let host_options = HostOptions::new(start_url, redirect_uris, options.ports.clone()); - let webview = Self::webview(host_options, window, proxy)?; + let webview = Self::webview(host_options, &window, proxy)?; event_loop.run(move |event, _, control_flow| { if let Some(timeout) = options.timeout.as_ref() { diff --git a/graph-rs-sdk-webview/src/main.rs b/graph-rs-sdk-webview/src/main.rs index a9f0311d..cf56e123 100644 --- a/graph-rs-sdk-webview/src/main.rs +++ b/graph-rs-sdk-webview/src/main.rs @@ -1,6 +1,6 @@ +use clap::Parser; use std::collections::HashSet; use std::time::Instant; -use clap::Parser; // `C:\Users\reeis\src\graph-rs\target\debug\graph-rs-sdk-webview.exe // cargo run --bin graph-rs-sdk-webview -- --window-title my_window_title From cd05f5f5fb4f21edf5e8c5e2057407d740f696b6 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:16:36 -0500 Subject: [PATCH 083/118] Fix wry/tao version sync and update for RawWindowHandle --- examples/drive/worksheet.rs | 3 - examples/interactive_auth/webview_options.rs | 2 +- graph-codegen/src/api_types/method_macro.rs | 1 - graph-codegen/src/api_types/request_client.rs | 1 - .../src/api_types/request_metadata.rs | 1 - graph-codegen/src/api_types/request_task.rs | 1 - .../src/api_types/write_configuration.rs | 1 - graph-core/src/resource/resource_identity.rs | 1 - graph-error/src/error.rs | 2 - graph-http/src/url/graphurl.rs | 1 - graph-oauth/Cargo.toml | 4 +- .../auth_code_authorization_url.rs | 9 +-- .../credentials/device_code_credential.rs | 55 ++++++------------- .../credentials/open_id_authorization_url.rs | 4 +- graph-oauth/src/identity/id_token.rs | 1 - graph-oauth/src/identity/token.rs | 28 +++++----- graph-oauth/src/interactive/mod.rs | 3 + graph-oauth/src/lib.rs | 2 +- graph-rs-sdk-webview/src/main.rs | 2 - src/client/graph.rs | 3 - 20 files changed, 45 insertions(+), 80 deletions(-) diff --git a/examples/drive/worksheet.rs b/examples/drive/worksheet.rs index a35dae01..0d6290ae 100644 --- a/examples/drive/worksheet.rs +++ b/examples/drive/worksheet.rs @@ -5,7 +5,6 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static DRIVE_ID: &str = "DRIVE_ID"; static ITEM_ID: &str = "ITEM_ID"; - pub async fn update_range_by_address() { let client = Graph::new(ACCESS_TOKEN); @@ -26,9 +25,7 @@ pub async fn update_range_by_address() { .await .unwrap(); - let workbook_json: serde_json::Value = resp.json().await.unwrap(); println!("workbook {:#?}", workbook_json); - } diff --git a/examples/interactive_auth/webview_options.rs b/examples/interactive_auth/webview_options.rs index 1a3f1fd5..190a2760 100644 --- a/examples/interactive_auth/webview_options.rs +++ b/examples/interactive_auth/webview_options.rs @@ -14,7 +14,7 @@ fn get_webview_options() -> WebViewOptions { // Give the window a title. The default is "Sign In" .window_title("Sign In") // OS specific theme. Windows only. - // See wry crate for more info. + // See Tao crate for more info. .theme(Theme::Dark) // Add a timeout that will close the window and return an error // when that timeout is reached. For instance, if your app is waiting on the diff --git a/graph-codegen/src/api_types/method_macro.rs b/graph-codegen/src/api_types/method_macro.rs index b5b62a0a..ab287176 100644 --- a/graph-codegen/src/api_types/method_macro.rs +++ b/graph-codegen/src/api_types/method_macro.rs @@ -3,7 +3,6 @@ use crate::parser::HttpMethod; use crate::settings::{GeneratedMacroType, MethodMacroModifier}; use from_as::*; use inflector::Inflector; -use std::convert::TryFrom; use std::io::{Read, Write}; /// Represents the macro used for describing requests. This is the outer diff --git a/graph-codegen/src/api_types/request_client.rs b/graph-codegen/src/api_types/request_client.rs index 4b8b8098..6b932c4e 100644 --- a/graph-codegen/src/api_types/request_client.rs +++ b/graph-codegen/src/api_types/request_client.rs @@ -2,7 +2,6 @@ use crate::api_types::RequestMetadata; use crate::traits::RequestParser; use from_as::*; use std::collections::{BTreeMap, VecDeque}; -use std::convert::TryFrom; use std::io::{Read, Write}; #[derive(Default, Debug, Clone, Serialize, Deserialize, FromFile, AsFile)] diff --git a/graph-codegen/src/api_types/request_metadata.rs b/graph-codegen/src/api_types/request_metadata.rs index e088a9bb..b1a6eb28 100644 --- a/graph-codegen/src/api_types/request_metadata.rs +++ b/graph-codegen/src/api_types/request_metadata.rs @@ -10,7 +10,6 @@ use crate::traits::{FilterMetadata, RequestParser, INTERNAL_PATH_ID}; use from_as::*; use graph_core::resource::ResourceIdentity; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; -use std::convert::TryFrom; use std::io::{Read, Write}; use std::str::FromStr; diff --git a/graph-codegen/src/api_types/request_task.rs b/graph-codegen/src/api_types/request_task.rs index 180de9bd..6d2eab3f 100644 --- a/graph-codegen/src/api_types/request_task.rs +++ b/graph-codegen/src/api_types/request_task.rs @@ -1,5 +1,4 @@ use from_as::*; -use std::convert::TryFrom; use std::io::{Read, Write}; /// Describes the type of action this request will perform. In some instances diff --git a/graph-codegen/src/api_types/write_configuration.rs b/graph-codegen/src/api_types/write_configuration.rs index ec716166..92f7eaca 100644 --- a/graph-codegen/src/api_types/write_configuration.rs +++ b/graph-codegen/src/api_types/write_configuration.rs @@ -2,7 +2,6 @@ use crate::api_types::ModFile; use crate::settings::ApiClientLinkSettings; use from_as::*; use graph_core::resource::ResourceIdentity; -use std::convert::TryFrom; use std::io::{Read, Write}; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, FromFile, AsFile)] diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 4310b836..9409deb9 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -1,5 +1,4 @@ use inflector::Inflector; -use std::convert::AsRef; /// Comprises both top level and second level resources. /// These are not generated from OpenApi, except for top level resources, diff --git a/graph-error/src/error.rs b/graph-error/src/error.rs index d35c0ed0..3643f0bd 100644 --- a/graph-error/src/error.rs +++ b/graph-error/src/error.rs @@ -1,8 +1,6 @@ use serde::Serialize; use std::fmt::{Display, Formatter}; -use std::string::ToString; - #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct InnerError { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/graph-http/src/url/graphurl.rs b/graph-http/src/url/graphurl.rs index 3f75779b..2247f6f3 100644 --- a/graph-http/src/url/graphurl.rs +++ b/graph-http/src/url/graphurl.rs @@ -1,6 +1,5 @@ use graph_error::GraphFailure; use std::ffi::OsStr; -use std::iter::Iterator; use std::ops::{Deref, Index, Range, RangeFrom, RangeFull, RangeTo}; use std::str::FromStr; use url::form_urlencoded::Serializer; diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 14424437..83ecf5fc 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -32,10 +32,10 @@ serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.25.0", features = ["derive"] } -tao = { version = "0.24.1", features = ["serde"] } +tao = { version = "0.25.0", features = ["serde"] } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } -wry = { version = "0.35.2", optional = true } +wry = { version = "0.36.0", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index ec0015f5..7a0940fb 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -60,7 +60,8 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// /// ```rust /// use uuid::Uuid; -/// use graph_oauth::oauth::{AzureCloudInstance, ConfidentialClientApplication, Prompt}; +/// use graph_oauth::{AzureCloudInstance, ConfidentialClientApplication, Prompt}; +/// use url::Url; /// /// let auth_url_builder = ConfidentialClientApplication::builder(Uuid::new_v4()) /// .auth_code_url_builder() @@ -68,7 +69,7 @@ credential_builder_base!(AuthCodeAuthorizationUrlParameterBuilder); /// .with_prompt(Prompt::Login) /// .with_state("1234") /// .with_scope(vec!["User.Read"]) -/// .with_redirect_uri("http://localhost:8000") +/// .with_redirect_uri(Url::parse("http://localhost:8000").unwrap()) /// .build(); /// /// let url = auth_url_builder.url(); @@ -353,10 +354,10 @@ mod internal { ) -> anyhow::Result<WebView> { let start_uri = host_options.start_uri.clone(); let validator = WebViewHostValidator::try_from(host_options)?; - Ok(WebViewBuilder::new(window)? + Ok(WebViewBuilder::new(window) .with_url(start_uri.as_ref())? // Disables file drop - .with_file_drop_handler(|_, _| true) + .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { if let Ok(url) = Url::parse(uri.as_str()) { let is_valid_host = validator.is_valid_uri(&url); diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index c6f64b26..01553fd8 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -397,20 +397,13 @@ impl DeviceCodePollingExecutor { sender.send(http_response).unwrap(); let device_code = device_code_response.device_code; - let interval = Duration::from_secs(device_code_response.interval); + let mut interval = Duration::from_secs(device_code_response.interval); credential.with_device_code(device_code); let _ = std::thread::spawn(move || { - let mut should_slow_down = false; - loop { // Wait the amount of seconds that interval is. - if should_slow_down { - should_slow_down = false; - std::thread::sleep(interval.add(Duration::from_secs(5))); - } else { - std::thread::sleep(interval); - } + std::thread::sleep(interval); let response = credential.execute().unwrap(); let http_response = response.into_http_response()?; @@ -427,13 +420,13 @@ impl DeviceCodePollingExecutor { if let Some(error) = option_error { match PollDeviceCodeEvent::from_str(error.as_str()) { Ok(poll_device_code_type) => match poll_device_code_type { - PollDeviceCodeEvent::AuthorizationPending => continue, - PollDeviceCodeEvent::AuthorizationDeclined => break, - PollDeviceCodeEvent::BadVerificationCode => continue, - PollDeviceCodeEvent::ExpiredToken => break, - PollDeviceCodeEvent::AccessDenied => break, + PollDeviceCodeEvent::AuthorizationPending + | PollDeviceCodeEvent::BadVerificationCode => continue, + PollDeviceCodeEvent::AuthorizationDeclined + | PollDeviceCodeEvent::ExpiredToken + | PollDeviceCodeEvent::AccessDenied => break, PollDeviceCodeEvent::SlowDown => { - should_slow_down = true; + interval = interval.add(Duration::from_secs(5)); continue; } }, @@ -487,17 +480,7 @@ impl DeviceCodePollingExecutor { credential.with_device_code(device_code); tokio::spawn(async move { - let mut should_slow_down = false; - loop { - // Should slow down is part of the openid connect spec and means that - // that we should wait longer between polling by the amount specified - // in the interval field of the device code. - if should_slow_down { - should_slow_down = false; - interval = interval.add(Duration::from_secs(5)); - } - // Wait the amount of seconds that interval is. tokio::time::sleep(interval).await; @@ -526,7 +509,10 @@ impl DeviceCodePollingExecutor { PollDeviceCodeEvent::ExpiredToken => break, PollDeviceCodeEvent::AccessDenied => break, PollDeviceCodeEvent::SlowDown => { - should_slow_down = true; + // Should slow down is part of the openid connect spec and means that + // that we should wait longer between polling by the amount specified + // in the interval field of the device code. + interval = interval.add(Duration::from_secs(5)); continue; } }, @@ -575,10 +561,10 @@ pub(crate) mod internal { window: &Window, _proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView> { - Ok(WebViewBuilder::new(window)? + Ok(WebViewBuilder::new(window) .with_url(host_options.start_uri.as_ref())? // Disables file drop - .with_file_drop_handler(|_, _| true) + .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { tracing::debug!(target: INTERACTIVE_AUTH, url = uri.as_str()); true @@ -638,18 +624,11 @@ impl DeviceCodeInteractiveAuth { &mut self, ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let mut credential = self.credential.clone(); - let interval = self.interval; - - let mut should_slow_down = false; + let mut interval = self.interval; loop { // Wait the amount of seconds that interval is. - if should_slow_down { - should_slow_down = false; - std::thread::sleep(interval.add(Duration::from_secs(5))); - } else { - std::thread::sleep(interval); - } + std::thread::sleep(interval); let response = credential.execute().unwrap(); let http_response = response.into_http_response().map_err(Box::new)?; @@ -672,7 +651,7 @@ impl DeviceCodeInteractiveAuth { PollDeviceCodeEvent::AuthorizationPending | PollDeviceCodeEvent::BadVerificationCode => continue, PollDeviceCodeEvent::SlowDown => { - should_slow_down = true; + interval = interval.add(Duration::from_secs(5)); continue; } PollDeviceCodeEvent::AuthorizationDeclined diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index a910f8ef..751e6cb0 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -408,10 +408,10 @@ impl WebViewAuth for OpenIdAuthorizationUrlParameters { ) -> anyhow::Result<WebView> { let start_uri = host_options.start_uri.clone(); let validator = WebViewHostValidator::try_from(host_options)?; - Ok(WebViewBuilder::new(window)? + Ok(WebViewBuilder::new(window) .with_url(start_uri.as_ref())? // Disables file drop - .with_file_drop_handler(|_, _| true) + .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { if let Ok(url) = Url::parse(uri.as_str()) { let is_valid_host = validator.is_valid_uri(&url); diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index 632d8520..0bd67fab 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -3,7 +3,6 @@ use serde::de::{Error, MapAccess, Visitor}; use serde::{Deserialize, Deserializer}; use serde_json::Value; use std::collections::HashMap; -use std::convert::TryFrom; use std::fmt::{Debug, Display, Formatter}; use crate::identity::AuthorizationResponse; diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 14ffc1ab..cd8d4ee7 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -60,7 +60,7 @@ struct PhantomToken { /// Create a new AccessToken. /// # Example /// ``` -/// # use graph_oauth::oauth::Token; +/// # use graph_oauth::Token; /// let token_response = Token::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); /// ``` /// The [Token::jwt] method attempts to parse the access token as a JWT. @@ -160,7 +160,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_token_type("Bearer"); @@ -174,7 +174,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_expires_in(3600); @@ -191,7 +191,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_scope(vec!["User.Read"]); @@ -205,7 +205,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_access_token("ASODFIUJ34KJ;LADSK"); @@ -219,7 +219,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_refresh_token("#ASOD323U5342"); @@ -233,7 +233,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.with_user_id("user_id"); @@ -247,7 +247,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{Token, IdToken}; + /// # use graph_oauth::{Token, IdToken}; /// /// let mut access_token = Token::default(); /// access_token.set_id_token("id_token"); @@ -274,8 +274,8 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; - /// # use graph_oauth::oauth::IdToken; + /// # use graph_oauth::Token; + /// # use graph_oauth::IdToken; /// /// let mut access_token = Token::default(); /// access_token.with_state("state"); @@ -314,7 +314,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// access_token.expires_in = 86999; @@ -333,7 +333,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.is_expired()); @@ -352,7 +352,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.is_expired_sub(time::Duration::minutes(5))); @@ -372,7 +372,7 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::Token; + /// # use graph_oauth::Token; /// /// let mut access_token = Token::default(); /// println!("{:#?}", access_token.elapsed()); diff --git a/graph-oauth/src/interactive/mod.rs b/graph-oauth/src/interactive/mod.rs index e58a5dae..828ba407 100644 --- a/graph-oauth/src/interactive/mod.rs +++ b/graph-oauth/src/interactive/mod.rs @@ -11,3 +11,6 @@ pub use interactive_auth::*; pub use webview_authorization_event::*; pub use webview_options::*; pub use with_interactive_auth::*; + +#[cfg(windows)] +pub use tao::window::Theme; diff --git a/graph-oauth/src/lib.rs b/graph-oauth/src/lib.rs index 2f148dfa..d66ce018 100644 --- a/graph-oauth/src/lib.rs +++ b/graph-oauth/src/lib.rs @@ -8,7 +8,7 @@ //! # Example ConfidentialClientApplication Authorization Code Flow //! ```rust //! use url::Url; -//! use graph_oauth::oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; +//! use graph_oauth::{AuthorizationCodeCredential, ConfidentialClientApplication}; //! //! pub fn authorization_url(client_id: &str) -> anyhow::Result<Url> { //! Ok(ConfidentialClientApplication::builder(client_id) diff --git a/graph-rs-sdk-webview/src/main.rs b/graph-rs-sdk-webview/src/main.rs index cf56e123..43d5b783 100644 --- a/graph-rs-sdk-webview/src/main.rs +++ b/graph-rs-sdk-webview/src/main.rs @@ -1,6 +1,4 @@ use clap::Parser; -use std::collections::HashSet; -use std::time::Instant; // `C:\Users\reeis\src\graph-rs\target\debug\graph-rs-sdk-webview.exe // cargo run --bin graph-rs-sdk-webview -- --window-title my_window_title diff --git a/src/client/graph.rs b/src/client/graph.rs index 61b43550..32acbd67 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -11,15 +11,12 @@ use crate::authentication_method_configurations::{ AuthenticationMethodConfigurationsApiClient, AuthenticationMethodConfigurationsIdApiClient, }; use crate::authentication_methods_policy::AuthenticationMethodsPolicyApiClient; - -use crate::api_default_imports::GraphClientConfiguration; use crate::batch::BatchApiClient; use crate::branding::BrandingApiClient; use crate::certificate_based_auth_configuration::{ CertificateBasedAuthConfigurationApiClient, CertificateBasedAuthConfigurationIdApiClient, }; use crate::chats::{ChatsApiClient, ChatsIdApiClient}; -use crate::client::ResourceProvisioner; use crate::communications::CommunicationsApiClient; use crate::contracts::{ContractsApiClient, ContractsIdApiClient}; use crate::data_policy_operations::DataPolicyOperationsApiClient; From e1ad7215916424983fd06de76d20f7968295660b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:43:48 -0500 Subject: [PATCH 084/118] Make tao dep optional enabled using interactive-auth feature --- graph-oauth/Cargo.toml | 4 ++-- graph-rs-sdk-webview/Cargo.toml | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 83ecf5fc..cc1635e5 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -32,7 +32,7 @@ serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.25.0", features = ["derive"] } -tao = { version = "0.25.0", features = ["serde"] } +tao = { version = "0.25.0", features = ["serde"], optional = true } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } wry = { version = "0.36.0", optional = true } @@ -51,7 +51,7 @@ brotli = ["reqwest/brotli", "graph-core/brotli"] deflate = ["reqwest/deflate", "graph-core/deflate"] trust-dns = ["reqwest/trust-dns", "graph-core/trust-dns"] openssl = ["dep:openssl"] -interactive-auth = ["dep:wry"] +interactive-auth = ["dep:wry", "dep:tao"] [[test]] name = "x509_certificate_tests" diff --git a/graph-rs-sdk-webview/Cargo.toml b/graph-rs-sdk-webview/Cargo.toml index 44f85ef4..a19597ef 100644 --- a/graph-rs-sdk-webview/Cargo.toml +++ b/graph-rs-sdk-webview/Cargo.toml @@ -8,8 +8,7 @@ homepage = "https://github.com/sreeise/graph-rs-sdk" [dependencies] clap = { version = "4.4.18", features = ["derive"] } -tao = { version = "0.24.1", features = ["serde"] } +# tao = { version = "0.24.1", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } url = { version = "2", features = ["serde"] } -wry = { version = "0.35.2" } - +# wry = { version = "0.35.2" } From 260ae22bcb057594253a5673362e7d34d6dda295 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Tue, 20 Feb 2024 01:54:26 -0500 Subject: [PATCH 085/118] Fix test for drive blocking client --- tests/enable_blocking_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/enable_blocking_client.rs b/tests/enable_blocking_client.rs index e18926a6..1e7394fb 100644 --- a/tests/enable_blocking_client.rs +++ b/tests/enable_blocking_client.rs @@ -18,7 +18,7 @@ fn drive() { assert!(response.status().is_success()); let body: serde_json::Value = response.json().unwrap(); assert_eq!(body["name"].as_str(), Some("update_test.docx")); - thread::sleep(Duration::from_secs(2)); + thread::sleep(Duration::from_secs(4)); let req = client .drive(id.as_str()) From 56cf89eeae08358accb9c938f18e7b4d7096bc24 Mon Sep 17 00:00:00 2001 From: Mike Potapenco <buhaytza2005@gmail.com> Date: Thu, 7 Mar 2024 20:43:33 +0000 Subject: [PATCH 086/118] docs: add example of sending message draft --- examples/drive/worksheet.rs | 3 --- examples/mail_folders_and_messages/messages.rs | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/examples/drive/worksheet.rs b/examples/drive/worksheet.rs index a35dae01..0d6290ae 100644 --- a/examples/drive/worksheet.rs +++ b/examples/drive/worksheet.rs @@ -5,7 +5,6 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static DRIVE_ID: &str = "DRIVE_ID"; static ITEM_ID: &str = "ITEM_ID"; - pub async fn update_range_by_address() { let client = Graph::new(ACCESS_TOKEN); @@ -26,9 +25,7 @@ pub async fn update_range_by_address() { .await .unwrap(); - let workbook_json: serde_json::Value = resp.json().await.unwrap(); println!("workbook {:#?}", workbook_json); - } diff --git a/examples/mail_folders_and_messages/messages.rs b/examples/mail_folders_and_messages/messages.rs index 5f4fe7ef..5630e966 100644 --- a/examples/mail_folders_and_messages/messages.rs +++ b/examples/mail_folders_and_messages/messages.rs @@ -1,4 +1,5 @@ use graph_rs_sdk::*; +use graph_rs_sdk::header::{HeaderValue, CONTENT_LENGTH}; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; @@ -93,6 +94,19 @@ pub async fn update_message() { println!("{response:#?}"); } +pub async fn send_message() { + let client = Graph::new(ACCESS_TOKEN); + + let response = client + .me() + .message(MESSAGE_ID) + .send() + .header(CONTENT_LENGTH, HeaderValue::from_str("0").unwrap()) + .send() + .await + .unwrap(); +} + pub async fn send_mail() -> GraphResult<()> { let client = Graph::new(ACCESS_TOKEN); From 87589e85f4615df7e2bbc44b9506053dc232dff9 Mon Sep 17 00:00:00 2001 From: Mike Potapenco <buhaytza2005@gmail.com> Date: Thu, 7 Mar 2024 20:51:30 +0000 Subject: [PATCH 087/118] Feat: add solutions and booking businesses codegen --- examples/drive/worksheet.rs | 3 - .../src/settings/resource_settings.rs | 27 +++++++++ graph-core/src/resource/resource_identity.rs | 1 + src/client/graph.rs | 3 + src/lib.rs | 1 + src/solutions/booking_businesses/mod.rs | 3 + src/solutions/booking_businesses/request.rs | 59 +++++++++++++++++++ src/solutions/mod.rs | 5 ++ src/solutions/request.rs | 22 +++++++ 9 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 src/solutions/booking_businesses/mod.rs create mode 100644 src/solutions/booking_businesses/request.rs create mode 100644 src/solutions/mod.rs create mode 100644 src/solutions/request.rs diff --git a/examples/drive/worksheet.rs b/examples/drive/worksheet.rs index a35dae01..0d6290ae 100644 --- a/examples/drive/worksheet.rs +++ b/examples/drive/worksheet.rs @@ -5,7 +5,6 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static DRIVE_ID: &str = "DRIVE_ID"; static ITEM_ID: &str = "ITEM_ID"; - pub async fn update_range_by_address() { let client = Graph::new(ACCESS_TOKEN); @@ -26,9 +25,7 @@ pub async fn update_range_by_address() { .await .unwrap(); - let workbook_json: serde_json::Value = resp.json().await.unwrap(); println!("workbook {:#?}", workbook_json); - } diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 220d93cd..80451524 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1185,6 +1185,21 @@ impl ResourceSettings { .api_client_links(get_users_api_client_links(ri)) .build() .unwrap(), + ResourceIdentity::Solutions => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::solutions::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("SolutionsApiClient"), + vec![ + ApiClientLink::Struct("booking_businesses", "BookingBusinessesApiClient"), + ApiClientLink::StructId("booking_business", "BookingBusinessesIdApiClient"), + ] + ) + ]) + .build() + .unwrap(), + ResourceIdentity::BookingBusinesses => ResourceSettings::builder(path_name, ri) + .build() + .unwrap(), _ => ResourceSettings::default(path_name, ri), } } @@ -2658,6 +2673,18 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .trim_path_start("/users/{user-id}") .build() .unwrap(), + ResourceIdentity::Solutions => WriteConfiguration::builder(resource_identity) + .filter_path(vec!["bookingBusinesses", "virtualEvents", "bookingCurrencies"]) + .children(vec![ + get_write_configuration(ResourceIdentity::BookingBusinesses), + ]) + .build() + .unwrap(), + ResourceIdentity::BookingBusinesses => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .filter_path(vec!["appointments", "calendarView", "customQuestions", "customers", "services", "staffMembers"]) + .trim_path_start("/solutions") + .build() + .unwrap(), _ => WriteConfiguration::builder(resource_identity) .build() .unwrap(), diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 4310b836..5daaefc0 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -50,6 +50,7 @@ pub enum ResourceIdentity { AuthenticationMethodConfigurations, AuthenticationMethodsPolicy, Batch, // Specifically for $batch requests. + BookingBusinesses, Branding, Buckets, CalendarGroups, diff --git a/src/client/graph.rs b/src/client/graph.rs index 196d3876..2e409d14 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -56,6 +56,7 @@ use crate::reports::ReportsApiClient; use crate::schema_extensions::{SchemaExtensionsApiClient, SchemaExtensionsIdApiClient}; use crate::service_principals::{ServicePrincipalsApiClient, ServicePrincipalsIdApiClient}; use crate::sites::{SitesApiClient, SitesIdApiClient}; +use crate::solutions::SolutionsApiClient; use crate::subscribed_skus::SubscribedSkusApiClient; use crate::subscriptions::{SubscriptionsApiClient, SubscriptionsIdApiClient}; use crate::teams::{TeamsApiClient, TeamsIdApiClient}; @@ -378,6 +379,8 @@ impl Graph { api_client_impl!(sites, SitesApiClient, site, SitesIdApiClient); + api_client_impl!(solutions, SolutionsApiClient); + api_client_impl!( subscribed_skus, SubscribedSkusApiClient, diff --git a/src/lib.rs b/src/lib.rs index d3e593b3..e02d1ea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,6 +288,7 @@ pub mod reports; pub mod schema_extensions; pub mod service_principals; pub mod sites; +pub mod solutions; pub mod subscribed_skus; pub mod subscriptions; pub mod teams; diff --git a/src/solutions/booking_businesses/mod.rs b/src/solutions/booking_businesses/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/booking_businesses/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs new file mode 100644 index 00000000..2739304d --- /dev/null +++ b/src/solutions/booking_businesses/request.rs @@ -0,0 +1,59 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); + +impl BookingBusinessesApiClient { + post!( + doc: "Create bookingBusiness", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "List bookingBusinesses", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: booking_businesses, + path: "/bookingBusinesses/$count" + ); +} + +impl BookingBusinessesIdApiClient { + delete!( + doc: "Delete bookingBusiness", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusiness", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update bookingbusiness", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); +} diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs new file mode 100644 index 00000000..9edfae1f --- /dev/null +++ b/src/solutions/mod.rs @@ -0,0 +1,5 @@ +mod request; +mod booking_businesses; + +pub use request::*; +pub use booking_businesses::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs new file mode 100644 index 00000000..a1442e00 --- /dev/null +++ b/src/solutions/request.rs @@ -0,0 +1,22 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); + +impl SolutionsApiClient {api_client_link!(booking_businesses, BookingBusinessesApiClient); +api_client_link_id!(booking_business, BookingBusinessesIdApiClient); + + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); +} From 357e2d330f841db50d7d91c38f58e5887d217a56 Mon Sep 17 00:00:00 2001 From: Mike Potapenco <buhaytza2005@gmail.com> Date: Thu, 7 Mar 2024 22:48:57 +0000 Subject: [PATCH 088/118] Feat: add appointment generation - appointments generated under `solutions` as there doesn't seem to be a way of creating a third level folder --- .../src/settings/resource_settings.rs | 14 ++++++ graph-core/src/resource/resource_identity.rs | 1 + src/solutions/appointments/mod.rs | 3 ++ src/solutions/appointments/request.rs | 49 +++++++++++++++++++ src/solutions/booking_businesses/request.rs | 5 +- src/solutions/mod.rs | 2 + src/solutions/request.rs | 4 +- 7 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 src/solutions/appointments/mod.rs create mode 100644 src/solutions/appointments/request.rs diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 80451524..9b504da3 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1198,6 +1198,15 @@ impl ResourceSettings { .build() .unwrap(), ResourceIdentity::BookingBusinesses => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::solutions::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("BookingBusinessesIdApiClient"), + vec![ + ApiClientLink::Struct("appointments", "AppointmentsApiClient"), + ApiClientLink::StructId("appointment", "AppointmentsIdApiClient"), + ] + ) + ]) .build() .unwrap(), _ => ResourceSettings::default(path_name, ri), @@ -2677,6 +2686,7 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .filter_path(vec!["bookingBusinesses", "virtualEvents", "bookingCurrencies"]) .children(vec![ get_write_configuration(ResourceIdentity::BookingBusinesses), + get_write_configuration(ResourceIdentity::Appointments), ]) .build() .unwrap(), @@ -2685,6 +2695,10 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .trim_path_start("/solutions") .build() .unwrap(), + ResourceIdentity::Appointments => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") + .build() + .unwrap(), _ => WriteConfiguration::builder(resource_identity) .build() .unwrap(), diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 5daaefc0..93cd4af1 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -41,6 +41,7 @@ pub enum ResourceIdentity { Application, Applications, ApplicationTemplates, + Appointments, AppRoleAssignments, AssignmentPolicies, AssignmentRequests, diff --git a/src/solutions/appointments/mod.rs b/src/solutions/appointments/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/appointments/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs new file mode 100644 index 00000000..08e0669d --- /dev/null +++ b/src/solutions/appointments/request.rs @@ -0,0 +1,49 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); + +impl AppointmentsApiClient { + post!( + doc: "Create bookingAppointment", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "List appointments", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: appointments, + path: "/appointments/$count" + ); +} + +impl AppointmentsIdApiClient { + delete!( + doc: "Delete bookingAppointment", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get bookingAppointment", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update bookingAppointment", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); +} diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index 2739304d..d6f2ef14 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -1,6 +1,7 @@ // GENERATED CODE use crate::api_default_imports::*; +use crate::solutions::*; resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); @@ -23,7 +24,9 @@ impl BookingBusinessesApiClient { ); } -impl BookingBusinessesIdApiClient { +impl BookingBusinessesIdApiClient {api_client_link_id!(appointment, AppointmentsIdApiClient); +api_client_link!(appointments, AppointmentsApiClient); + delete!( doc: "Delete bookingBusiness", name: delete_booking_businesses, diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index 9edfae1f..234ee4c7 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,5 +1,7 @@ mod request; mod booking_businesses; +mod appointments; pub use request::*; pub use booking_businesses::*; +pub use appointments::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index a1442e00..d8ef6951 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,8 +5,8 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient {api_client_link!(booking_businesses, BookingBusinessesApiClient); -api_client_link_id!(booking_business, BookingBusinessesIdApiClient); +impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); +api_client_link!(booking_businesses, BookingBusinessesApiClient); get!( doc: "Get solutions", From d60521817e0cd50501ee983bac788e747d3017e9 Mon Sep 17 00:00:00 2001 From: Mike Potapenco <buhaytza2005@gmail.com> Date: Thu, 7 Mar 2024 23:47:42 +0000 Subject: [PATCH 089/118] feat: add services codegen --- .../src/settings/resource_settings.rs | 7 +++ graph-core/src/resource/resource_identity.rs | 1 + src/solutions/booking_businesses/request.rs | 2 + src/solutions/mod.rs | 2 + src/solutions/request.rs | 4 +- src/solutions/services/mod.rs | 3 ++ src/solutions/services/request.rs | 43 +++++++++++++++++++ 7 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/solutions/services/mod.rs create mode 100644 src/solutions/services/request.rs diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 9b504da3..0b1f415e 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1204,6 +1204,8 @@ impl ResourceSettings { vec![ ApiClientLink::Struct("appointments", "AppointmentsApiClient"), ApiClientLink::StructId("appointment", "AppointmentsIdApiClient"), + ApiClientLink::Struct("services", "ServicesApiClient"), + ApiClientLink::StructId("service", "ServicesIdApiClient"), ] ) ]) @@ -2687,6 +2689,7 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .children(vec![ get_write_configuration(ResourceIdentity::BookingBusinesses), get_write_configuration(ResourceIdentity::Appointments), + get_write_configuration(ResourceIdentity::Services), ]) .build() .unwrap(), @@ -2699,6 +2702,10 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") .build() .unwrap(), + ResourceIdentity::Services => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") + .build() + .unwrap(), _ => WriteConfiguration::builder(resource_identity) .build() .unwrap(), diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 93cd4af1..5b404dd8 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -203,6 +203,7 @@ pub enum ResourceIdentity { Security, ServicePrincipals, ServicePrincipalsOwners, + Services, Settings, SharedWithTeams, Shares, diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index d6f2ef14..7bb1fe57 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -26,6 +26,8 @@ impl BookingBusinessesApiClient { impl BookingBusinessesIdApiClient {api_client_link_id!(appointment, AppointmentsIdApiClient); api_client_link!(appointments, AppointmentsApiClient); +api_client_link_id!(service, ServicesIdApiClient); +api_client_link!(services, ServicesApiClient); delete!( doc: "Delete bookingBusiness", diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index 234ee4c7..89ce4ac5 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,7 +1,9 @@ mod request; mod booking_businesses; mod appointments; +mod services; pub use request::*; pub use booking_businesses::*; pub use appointments::*; +pub use services::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index d8ef6951..a1442e00 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,8 +5,8 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); -api_client_link!(booking_businesses, BookingBusinessesApiClient); +impl SolutionsApiClient {api_client_link!(booking_businesses, BookingBusinessesApiClient); +api_client_link_id!(booking_business, BookingBusinessesIdApiClient); get!( doc: "Get solutions", diff --git a/src/solutions/services/mod.rs b/src/solutions/services/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/services/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs new file mode 100644 index 00000000..bc1035dd --- /dev/null +++ b/src/solutions/services/request.rs @@ -0,0 +1,43 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); + +impl ServicesApiClient { + post!( + doc: "Create bookingService", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "List services", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: services, + path: "/services/$count" + ); +} + +impl ServicesIdApiClient { + delete!( + doc: "Delete bookingService", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get bookingService", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update bookingservice", + name: update_services, + path: "/services/{{RID}}", + body: true + ); +} From eea8fe8e72d4d08e550a33805a6ea8f7bf9a1d5f Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 7 Mar 2024 23:33:30 -0500 Subject: [PATCH 090/118] Updates to docs and conventions across clients --- Cargo.toml | 3 +- .../client_credentials.rs | 3 +- .../client_credentials_admin_consent.rs | 8 ++-- .../identity_platform_auth/openid/openid.rs | 4 +- .../openid/server_examples/openid.rs | 1 - graph-oauth/Cargo.toml | 4 +- .../auth_code_authorization_url.rs | 2 +- .../authorization_code_credential.rs | 7 +--- .../client_credentials_authorization_url.rs | 6 +-- .../confidential_client_application.rs | 2 +- .../credentials/device_code_credential.rs | 2 +- .../src/identity/credentials/display.rs | 17 --------- graph-oauth/src/identity/credentials/mod.rs | 2 - .../credentials/open_id_authorization_url.rs | 2 +- .../credentials/open_id_credential.rs | 10 ++--- graph-oauth/src/identity/token.rs | 6 +-- graph-rs-sdk-webview/Cargo.toml | 14 ------- graph-rs-sdk-webview/src/main.rs | 38 ------------------- src/client/graph.rs | 6 ++- 19 files changed, 30 insertions(+), 107 deletions(-) delete mode 100644 graph-oauth/src/identity/credentials/display.rs delete mode 100644 graph-rs-sdk-webview/Cargo.toml delete mode 100644 graph-rs-sdk-webview/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 715cd2fc..2679f696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,7 @@ members = [ "test-tools", "graph-codegen", "graph-http", - "graph-core", - "graph-rs-sdk-webview" + "graph-core" ] [dependencies] diff --git a/examples/authorization_sign_in/client_credentials.rs b/examples/authorization_sign_in/client_credentials.rs index b3effb47..67b33634 100644 --- a/examples/authorization_sign_in/client_credentials.rs +++ b/examples/authorization_sign_in/client_credentials.rs @@ -2,6 +2,7 @@ use graph_rs_sdk::{ error::IdentityResult, identity::{ClientCredentialsAuthorizationUrlParameters, ClientSecretCredential}, }; +use url::Url; // The client_id must be changed before running this example. static CLIENT_ID: &str = "<CLIENT_ID>"; @@ -17,7 +18,7 @@ fn get_admin_consent_url() -> IdentityResult<url::Url> { // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. fn get_admin_consent_url_from_builder() -> IdentityResult<url::Url> { let url_builder = ClientSecretCredential::authorization_url_builder(CLIENT_ID) - .with_redirect_uri(REDIRECT_URI)? + .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) .with_state("123") .with_tenant("tenant_id") .build(); diff --git a/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs index 9c5ebb7f..4cfd8510 100644 --- a/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs +++ b/examples/identity_platform_auth/client_credentials/server_examples/client_credentials_admin_consent.rs @@ -20,8 +20,8 @@ // used to get access tokens programmatically without any consent by a user // or admin. See examples/client_credentials.rs -use graph_rs_sdk::error::IdentityResult; use graph_rs_sdk::identity::{ClientCredentialAdminConsentResponse, ConfidentialClientApplication}; +use url::Url; use warp::Filter; // The client_id must be changed before running this example. @@ -34,14 +34,14 @@ static REDIRECT_URI: &str = "http://localhost:8000/redirect"; // OR use the builder: // Use the builder if you want to set a specific tenant, or a state, or set a specific Authority. -fn get_admin_consent_url() -> IdentityResult<url::Url> { +fn get_admin_consent_url() -> anyhow::Result<url::Url> { let url_builder = ConfidentialClientApplication::builder(CLIENT_ID) .client_credential_url_builder() - .with_redirect_uri(REDIRECT_URI)? + .with_redirect_uri(Url::parse(REDIRECT_URI)?) .with_state("123") .with_tenant(TENANT_ID) .build(); - url_builder.url() + Ok(url_builder.url()?) } // ------------------------------------------------------------------------------------------------- diff --git a/examples/identity_platform_auth/openid/openid.rs b/examples/identity_platform_auth/openid/openid.rs index 92e4ad18..ea51a7f5 100644 --- a/examples/identity_platform_auth/openid/openid.rs +++ b/examples/identity_platform_auth/openid/openid.rs @@ -1,12 +1,13 @@ use graph_rs_sdk::identity::{ConfidentialClientApplication, IdToken}; use graph_rs_sdk::GraphClient; +use url::Url; // OpenIdCredential will automatically include the openid scope fn get_graph_client( tenant_id: &str, client_id: &str, client_secret: &str, - redirect_uri: &str, + redirect_uri: Url, scope: Vec<&str>, id_token: IdToken, ) -> GraphClient { @@ -14,7 +15,6 @@ fn get_graph_client( .with_openid(id_token.code.unwrap(), client_secret) .with_tenant(tenant_id) .with_redirect_uri(redirect_uri) - .unwrap() .with_scope(scope) .build(); diff --git a/examples/identity_platform_auth/openid/server_examples/openid.rs b/examples/identity_platform_auth/openid/server_examples/openid.rs index 92a78031..0798f57a 100644 --- a/examples/identity_platform_auth/openid/server_examples/openid.rs +++ b/examples/identity_platform_auth/openid/server_examples/openid.rs @@ -64,7 +64,6 @@ async fn handle_redirect(mut id_token: IdToken) -> Result<Box<dyn warp::Reply>, .with_openid(code, CLIENT_SECRET) .with_tenant(TENANT_ID) .with_redirect_uri(Url::parse(REDIRECT_URI).unwrap()) - .unwrap() .with_scope(vec!["User.Read", "User.ReadWrite"]) // OpenIdCredential automatically sets the openid scope .build(); diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index cc1635e5..e0c5aaa1 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -32,10 +32,10 @@ serde-aux = "4.1.2" serde_json = "1" serde_urlencoded = "0.7.1" strum = { version = "0.25.0", features = ["derive"] } -tao = { version = "0.25.0", features = ["serde"], optional = true } +tao = { version = "0.26.1", features = ["serde"], optional = true } url = { version = "2", features = ["serde"] } time = { version = "0.3.10", features = ["local-offset", "serde"] } -wry = { version = "0.36.0", optional = true } +wry = { version = "0.37.0", optional = true } uuid = { version = "1.3.1", features = ["v4", "serde"] } tokio = { version = "1.27.0", features = ["full"] } tracing = "0.1.37" diff --git a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs index 7a0940fb..00816062 100644 --- a/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/auth_code_authorization_url.rs @@ -355,7 +355,7 @@ mod internal { let start_uri = host_options.start_uri.clone(); let validator = WebViewHostValidator::try_from(host_options)?; Ok(WebViewBuilder::new(window) - .with_url(start_uri.as_ref())? + .with_url(start_uri.as_ref()) // Disables file drop .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { diff --git a/graph-oauth/src/identity/credentials/authorization_code_credential.rs b/graph-oauth/src/identity/credentials/authorization_code_credential.rs index 5cd1d3d5..ea65769d 100644 --- a/graph-oauth/src/identity/credentials/authorization_code_credential.rs +++ b/graph-oauth/src/identity/credentials/authorization_code_credential.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Formatter}; use async_trait::async_trait; use http::{HeaderMap, HeaderName, HeaderValue}; -use reqwest::IntoUrl; + use url::Url; use uuid::Uuid; @@ -92,11 +92,8 @@ impl AuthorizationCodeCredential { client_id: impl AsRef<str>, client_secret: impl AsRef<str>, authorization_code: impl AsRef<str>, - redirect_uri: impl IntoUrl, + redirect_uri: Url, ) -> IdentityResult<AuthorizationCodeCredential> { - let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; - Ok(AuthorizationCodeCredential { app_config: AppConfigBuilder::new(client_id.as_ref()) .tenant(tenant_id.as_ref()) diff --git a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs index c5e48580..3137b66e 100644 --- a/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/client_credentials_authorization_url.rs @@ -137,11 +137,9 @@ impl ClientCredentialsAuthorizationUrlParameterBuilder { Ok(self) } - pub fn with_redirect_uri<T: IntoUrl>(&mut self, redirect_uri: T) -> IdentityResult<&mut Self> { - let redirect_uri_result = Url::parse(redirect_uri.as_str()); - let redirect_uri = redirect_uri.into_url().or(redirect_uri_result)?; + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { self.credential.app_config.redirect_uri = Some(redirect_uri); - Ok(self) + self } /// Convenience method. Same as calling [with_authority(Authority::TenantId("tenant_id"))] diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index 350c7536..df3bc437 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -177,7 +177,7 @@ impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> } impl ConfidentialClientApplication<OpenIdCredential> { - pub fn decoded_id_token(&self) -> Option<&TokenData<Claims>> { + pub fn decoded_id_token(&self) -> Option<TokenData<Claims>> { self.credential.get_decoded_jwt() } } diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 01553fd8..75dadfb5 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -562,7 +562,7 @@ pub(crate) mod internal { _proxy: EventLoopProxy<UserEvents>, ) -> anyhow::Result<WebView> { Ok(WebViewBuilder::new(window) - .with_url(host_options.start_uri.as_ref())? + .with_url(host_options.start_uri.as_ref()) // Disables file drop .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { diff --git a/graph-oauth/src/identity/credentials/display.rs b/graph-oauth/src/identity/credentials/display.rs deleted file mode 100644 index a0451cce..00000000 --- a/graph-oauth/src/identity/credentials/display.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub enum Display { - /// The Authorization Server SHOULD display the authentication and consent UI - /// consistent with a full User Agent page view. If the display parameter is not - /// specified, this is the default display mode. - Page, - /// The Authorization Server SHOULD display the authentication and consent UI consistent with - /// a popup User Agent window. The popup User Agent window should be of an appropriate size - /// for a login-focused dialog and should not obscure the entire window that it is popping - /// up over. - Popup, - /// The Authorization Server SHOULD display the authentication and consent UI consistent with - /// a device that leverages a touch interface. - Touch, - /// The Authorization Server SHOULD display the authentication and consent UI consistent with - /// a "feature phone" type display. - Wap, -} diff --git a/graph-oauth/src/identity/credentials/mod.rs b/graph-oauth/src/identity/credentials/mod.rs index 25267503..d6ad0bc9 100644 --- a/graph-oauth/src/identity/credentials/mod.rs +++ b/graph-oauth/src/identity/credentials/mod.rs @@ -13,7 +13,6 @@ pub use client_credentials_authorization_url::*; pub use client_secret_credential::*; pub use confidential_client_application::*; pub use device_code_credential::*; -pub use display::*; pub use environment_credential::*; pub use open_id_authorization_url::*; pub use open_id_credential::*; @@ -45,7 +44,6 @@ mod client_credentials_authorization_url; mod client_secret_credential; mod confidential_client_application; mod device_code_credential; -mod display; mod environment_credential; mod open_id_authorization_url; mod open_id_credential; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 751e6cb0..8fbdbfb6 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -409,7 +409,7 @@ impl WebViewAuth for OpenIdAuthorizationUrlParameters { let start_uri = host_options.start_uri.clone(); let validator = WebViewHostValidator::try_from(host_options)?; Ok(WebViewBuilder::new(window) - .with_url(start_uri.as_ref())? + .with_url(start_uri.as_ref()) // Disables file drop .with_file_drop_handler(|_| true) .with_navigation_handler(move |uri| { diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index 94b116b7..ca0b06fe 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -126,8 +126,8 @@ impl OpenIdCredential { self.pkce.as_ref() } - pub(crate) fn get_decoded_jwt(&self) -> Option<&TokenData<Claims>> { - self.id_token_jwt.as_ref() + pub(crate) fn get_decoded_jwt(&self) -> Option<TokenData<Claims>> { + self.id_token_jwt.clone() } pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { @@ -723,9 +723,9 @@ impl OpenIdCredentialBuilder { self } - pub fn with_redirect_uri<U: IntoUrl>(&mut self, redirect_uri: U) -> anyhow::Result<&mut Self> { - self.credential.app_config.redirect_uri = Some(redirect_uri.into_url()?); - Ok(self) + pub fn with_redirect_uri(&mut self, redirect_uri: Url) -> &mut Self { + self.credential.app_config.redirect_uri = Some(redirect_uri); + self } pub fn with_client_secret<T: AsRef<str>>(&mut self, client_secret: T) -> &mut Self { diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index cd8d4ee7..8e8deb3c 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -63,10 +63,8 @@ struct PhantomToken { /// # use graph_oauth::Token; /// let token_response = Token::new("Bearer", 3600, "ASODFIUJ34KJ;LADSK", vec!["User.Read"]); /// ``` -/// The [Token::jwt] method attempts to parse the access token as a JWT. -/// Tokens returned for personal microsoft accounts that use legacy MSA -/// are encrypted and cannot be parsed. This bearer token may still be -/// valid but the jwt() method will return None. +/// The [Token::decode] method parses the id token into a JWT and returns it. Calling +/// [Token::decode] when the [Token]'s `id_token` field is None returns an error result. /// For more info see: /// [Microsoft identity platform access tokens](https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens) /// ``` diff --git a/graph-rs-sdk-webview/Cargo.toml b/graph-rs-sdk-webview/Cargo.toml deleted file mode 100644 index a19597ef..00000000 --- a/graph-rs-sdk-webview/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "graph-rs-sdk-webview" -version = "0.1.0" -edition = "2021" -repository = "https://github.com/sreeise/graph-rs-sdk" -description = "Webview interactive auth for Microsoft identity platform" -homepage = "https://github.com/sreeise/graph-rs-sdk" - -[dependencies] -clap = { version = "4.4.18", features = ["derive"] } -# tao = { version = "0.24.1", features = ["serde"] } -time = { version = "0.3.10", features = ["local-offset", "serde"] } -url = { version = "2", features = ["serde"] } -# wry = { version = "0.35.2" } diff --git a/graph-rs-sdk-webview/src/main.rs b/graph-rs-sdk-webview/src/main.rs deleted file mode 100644 index 43d5b783..00000000 --- a/graph-rs-sdk-webview/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -use clap::Parser; - -// `C:\Users\reeis\src\graph-rs\target\debug\graph-rs-sdk-webview.exe -// cargo run --bin graph-rs-sdk-webview -- --window-title my_window_title - -fn main() { - let args = Args::parse(); - - println!("{:#?}", args.window_title); -} - -#[derive(Debug, Parser)] -#[command(name = "graph-rs-sdk-webview")] -#[command(author, version)] -struct Args { - #[arg(short, long)] - window_title: Option<String>, - - #[arg(short, long)] - ports: Option<String>, - - /// Add a timeout that will close the window and return an error - /// when that timeout is reached. For instance, if your app is waiting on the - /// user to log in and the user has not logged in after 20 minutes you may - /// want to assume the user is idle in some way and close out of the webview window. - /// - /// Default is no timeout. - //#[arg(short, long)] - ///pub timeout: Option<Instant>, - - /// The webview can store the cookies that were set after sign in so that on the next - /// sign in the user is automatically logged in through SSO. Or you can clear the browsing - /// data, cookies in this case, after sign in when the webview window closes. - /// - /// Default is false - #[arg(short, long)] - pub clear_data: bool, -} diff --git a/src/client/graph.rs b/src/client/graph.rs index 32acbd67..bb4a6384 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -211,7 +211,8 @@ impl GraphClient { /// and recorded. /// /// You should also assume China's Graph API operated by 21Vianet is being monitored - /// by the Chinese government (who controls all Chinese companies and citizens). + /// by the Chinese government who is well known for the control it has over Chinese companies + /// and for its surveillance state of Chinese citizens. /// And, according to Microsoft, **These services are subject to Chinese laws**. See /// [Microsoft 365 operated by 21Vianet](https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/microsoft-365-operated-by-21vianet) /// @@ -254,7 +255,8 @@ impl GraphClient { /// and recorded. /// /// You should also assume China's Graph API operated by 21Vianet is being monitored - /// by the Chinese government (who controls all Chinese companies and citizens). + /// by the Chinese government who is well known for the control it has over Chinese companies + /// and for its surveillance state of Chinese citizens. /// And, according to Microsoft, **These services are subject to Chinese laws**. See /// [Microsoft 365 operated by 21Vianet](https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/microsoft-365-operated-by-21vianet) /// From 317d34cf54dd8a9fe690bf8fa3854ffaf968d800 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 8 Mar 2024 06:11:43 +0000 Subject: [PATCH 091/118] fix: add print statement to send email example Keeps the compiler happy about unused variables --- examples/mail_folders_and_messages/messages.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/mail_folders_and_messages/messages.rs b/examples/mail_folders_and_messages/messages.rs index 5630e966..5d156237 100644 --- a/examples/mail_folders_and_messages/messages.rs +++ b/examples/mail_folders_and_messages/messages.rs @@ -105,6 +105,8 @@ pub async fn send_message() { .send() .await .unwrap(); + + println!("{response:#?}"); } pub async fn send_mail() -> GraphResult<()> { From 2b2304d9e1b7ffdb70c4f33f449a50fc7b213ea8 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 8 Mar 2024 06:55:28 +0000 Subject: [PATCH 092/118] Add generation: Customers, CustomQuestions, StaffMembers --- .../src/settings/resource_settings.rs | 21 ++++++ graph-core/src/resource/resource_identity.rs | 3 + src/client/graph.rs | 4 +- src/lib.rs | 2 +- src/solutions/appointments/mod.rs | 3 - src/solutions/appointments/request.rs | 49 -------------- src/solutions/booking_businesses/mod.rs | 3 - src/solutions/booking_businesses/request.rs | 64 ------------------- src/solutions/mod.rs | 9 --- src/solutions/request.rs | 22 ------- src/solutions/services/mod.rs | 3 - src/solutions/services/request.rs | 43 ------------- 12 files changed, 27 insertions(+), 199 deletions(-) delete mode 100644 src/solutions/appointments/mod.rs delete mode 100644 src/solutions/appointments/request.rs delete mode 100644 src/solutions/booking_businesses/mod.rs delete mode 100644 src/solutions/booking_businesses/request.rs delete mode 100644 src/solutions/mod.rs delete mode 100644 src/solutions/request.rs delete mode 100644 src/solutions/services/mod.rs delete mode 100644 src/solutions/services/request.rs diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 0b1f415e..d3197c14 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1206,6 +1206,12 @@ impl ResourceSettings { ApiClientLink::StructId("appointment", "AppointmentsIdApiClient"), ApiClientLink::Struct("services", "ServicesApiClient"), ApiClientLink::StructId("service", "ServicesIdApiClient"), + ApiClientLink::Struct("custom_questions", "CustomQuestionsApiClient"), + ApiClientLink::StructId("custom_question", "CustomQuestionIdApiClient"), + ApiClientLink::Struct("customers", "CustomersApiClient"), + ApiClientLink::StructId("customer", "CustomersIdApiClient"), + ApiClientLink::Struct("staff_members", "StaffMembersApiClient"), + ApiClientLink::StructId("staff_member", "StaffMembersIdApiClient"), ] ) ]) @@ -2690,6 +2696,9 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf get_write_configuration(ResourceIdentity::BookingBusinesses), get_write_configuration(ResourceIdentity::Appointments), get_write_configuration(ResourceIdentity::Services), + get_write_configuration(ResourceIdentity::CustomQuestions), + get_write_configuration(ResourceIdentity::Customers), + get_write_configuration(ResourceIdentity::StaffMembers), ]) .build() .unwrap(), @@ -2706,6 +2715,18 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") .build() .unwrap(), + ResourceIdentity::CustomQuestions => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") + .build() + .unwrap(), + ResourceIdentity::Customers=> WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") + .build() + .unwrap(), + ResourceIdentity::StaffMembers => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/bookingBusinesses/{bookingBusiness-id}") + .build() + .unwrap(), _ => WriteConfiguration::builder(resource_identity) .build() .unwrap(), diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 5b404dd8..9ae40d44 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -81,6 +81,8 @@ pub enum ResourceIdentity { CreatedByUser, CreatedObjects, Custom, + Customers, + CustomQuestions, DataPolicyOperations, DefaultCalendar, DefaultManagedAppProtections, @@ -213,6 +215,7 @@ pub enum ResourceIdentity { SitesItemsVersions, SitesLists, Solutions, + StaffMembers, SubscribedSkus, Subscriptions, Tabs, diff --git a/src/client/graph.rs b/src/client/graph.rs index 2e409d14..ba84a7f1 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -56,7 +56,7 @@ use crate::reports::ReportsApiClient; use crate::schema_extensions::{SchemaExtensionsApiClient, SchemaExtensionsIdApiClient}; use crate::service_principals::{ServicePrincipalsApiClient, ServicePrincipalsIdApiClient}; use crate::sites::{SitesApiClient, SitesIdApiClient}; -use crate::solutions::SolutionsApiClient; +//use crate::solutions::SolutionsApiClient; use crate::subscribed_skus::SubscribedSkusApiClient; use crate::subscriptions::{SubscriptionsApiClient, SubscriptionsIdApiClient}; use crate::teams::{TeamsApiClient, TeamsIdApiClient}; @@ -379,7 +379,7 @@ impl Graph { api_client_impl!(sites, SitesApiClient, site, SitesIdApiClient); - api_client_impl!(solutions, SolutionsApiClient); + //api_client_impl!(solutions, SolutionsApiClient); api_client_impl!( subscribed_skus, diff --git a/src/lib.rs b/src/lib.rs index e02d1ea3..9fcb42d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ pub mod reports; pub mod schema_extensions; pub mod service_principals; pub mod sites; -pub mod solutions; +//pub mod solutions; pub mod subscribed_skus; pub mod subscriptions; pub mod teams; diff --git a/src/solutions/appointments/mod.rs b/src/solutions/appointments/mod.rs deleted file mode 100644 index 3edd9a21..00000000 --- a/src/solutions/appointments/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod request; - -pub use request::*; diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs deleted file mode 100644 index 08e0669d..00000000 --- a/src/solutions/appointments/request.rs +++ /dev/null @@ -1,49 +0,0 @@ -// GENERATED CODE - -use crate::api_default_imports::*; - -resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); - -impl AppointmentsApiClient { - post!( - doc: "Create bookingAppointment", - name: create_appointments, - path: "/appointments", - body: true - ); - get!( - doc: "List appointments", - name: list_appointments, - path: "/appointments" - ); - get!( - doc: "Get the number of the resource", - name: appointments, - path: "/appointments/$count" - ); -} - -impl AppointmentsIdApiClient { - delete!( - doc: "Delete bookingAppointment", - name: delete_appointments, - path: "/appointments/{{RID}}" - ); - get!( - doc: "Get bookingAppointment", - name: get_appointments, - path: "/appointments/{{RID}}" - ); - patch!( - doc: "Update bookingAppointment", - name: update_appointments, - path: "/appointments/{{RID}}", - body: true - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/appointments/{{RID}}/cancel", - body: true - ); -} diff --git a/src/solutions/booking_businesses/mod.rs b/src/solutions/booking_businesses/mod.rs deleted file mode 100644 index 3edd9a21..00000000 --- a/src/solutions/booking_businesses/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod request; - -pub use request::*; diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs deleted file mode 100644 index 7bb1fe57..00000000 --- a/src/solutions/booking_businesses/request.rs +++ /dev/null @@ -1,64 +0,0 @@ -// GENERATED CODE - -use crate::api_default_imports::*; -use crate::solutions::*; - -resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); - -impl BookingBusinessesApiClient { - post!( - doc: "Create bookingBusiness", - name: create_booking_businesses, - path: "/bookingBusinesses", - body: true - ); - get!( - doc: "List bookingBusinesses", - name: list_booking_businesses, - path: "/bookingBusinesses" - ); - get!( - doc: "Get the number of the resource", - name: booking_businesses, - path: "/bookingBusinesses/$count" - ); -} - -impl BookingBusinessesIdApiClient {api_client_link_id!(appointment, AppointmentsIdApiClient); -api_client_link!(appointments, AppointmentsApiClient); -api_client_link_id!(service, ServicesIdApiClient); -api_client_link!(services, ServicesApiClient); - - delete!( - doc: "Delete bookingBusiness", - name: delete_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - get!( - doc: "Get bookingBusiness", - name: get_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - patch!( - doc: "Update bookingbusiness", - name: update_booking_businesses, - path: "/bookingBusinesses/{{RID}}", - body: true - ); - post!( - doc: "Invoke action getStaffAvailability", - name: get_staff_availability, - path: "/bookingBusinesses/{{RID}}/getStaffAvailability", - body: true - ); - post!( - doc: "Invoke action publish", - name: publish, - path: "/bookingBusinesses/{{RID}}/publish" - ); - post!( - doc: "Invoke action unpublish", - name: unpublish, - path: "/bookingBusinesses/{{RID}}/unpublish" - ); -} diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs deleted file mode 100644 index 89ce4ac5..00000000 --- a/src/solutions/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod request; -mod booking_businesses; -mod appointments; -mod services; - -pub use request::*; -pub use booking_businesses::*; -pub use appointments::*; -pub use services::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs deleted file mode 100644 index a1442e00..00000000 --- a/src/solutions/request.rs +++ /dev/null @@ -1,22 +0,0 @@ -// GENERATED CODE - -use crate::api_default_imports::*; -use crate::solutions::*; - -resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); - -impl SolutionsApiClient {api_client_link!(booking_businesses, BookingBusinessesApiClient); -api_client_link_id!(booking_business, BookingBusinessesIdApiClient); - - get!( - doc: "Get solutions", - name: get_solutions_root, - path: "/solutions" - ); - patch!( - doc: "Update solutions", - name: update_solutions_root, - path: "/solutions", - body: true - ); -} diff --git a/src/solutions/services/mod.rs b/src/solutions/services/mod.rs deleted file mode 100644 index 3edd9a21..00000000 --- a/src/solutions/services/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod request; - -pub use request::*; diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs deleted file mode 100644 index bc1035dd..00000000 --- a/src/solutions/services/request.rs +++ /dev/null @@ -1,43 +0,0 @@ -// GENERATED CODE - -use crate::api_default_imports::*; - -resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); - -impl ServicesApiClient { - post!( - doc: "Create bookingService", - name: create_services, - path: "/services", - body: true - ); - get!( - doc: "List services", - name: list_services, - path: "/services" - ); - get!( - doc: "Get the number of the resource", - name: services, - path: "/services/$count" - ); -} - -impl ServicesIdApiClient { - delete!( - doc: "Delete bookingService", - name: delete_services, - path: "/services/{{RID}}" - ); - get!( - doc: "Get bookingService", - name: get_services, - path: "/services/{{RID}}" - ); - patch!( - doc: "Update bookingservice", - name: update_services, - path: "/services/{{RID}}", - body: true - ); -} From f4ca2df4b29c0ee2338c6f4b1b77d23f6c7a940a Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 8 Mar 2024 06:57:55 +0000 Subject: [PATCH 093/118] relink solutions --- src/client/graph.rs | 2 +- src/lib.rs | 2 +- src/solutions/appointments/mod.rs | 3 + src/solutions/appointments/request.rs | 53 +++++++++++++++ src/solutions/booking_businesses/mod.rs | 3 + src/solutions/booking_businesses/request.rs | 75 +++++++++++++++++++++ src/solutions/custom_questions/mod.rs | 3 + src/solutions/custom_questions/request.rs | 47 +++++++++++++ src/solutions/customers/mod.rs | 3 + src/solutions/customers/request.rs | 47 +++++++++++++ src/solutions/mod.rs | 15 +++++ src/solutions/request.rs | 23 +++++++ src/solutions/services/mod.rs | 3 + src/solutions/services/request.rs | 47 +++++++++++++ src/solutions/staff_members/mod.rs | 3 + src/solutions/staff_members/request.rs | 47 +++++++++++++ 16 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 src/solutions/appointments/mod.rs create mode 100644 src/solutions/appointments/request.rs create mode 100644 src/solutions/booking_businesses/mod.rs create mode 100644 src/solutions/booking_businesses/request.rs create mode 100644 src/solutions/custom_questions/mod.rs create mode 100644 src/solutions/custom_questions/request.rs create mode 100644 src/solutions/customers/mod.rs create mode 100644 src/solutions/customers/request.rs create mode 100644 src/solutions/mod.rs create mode 100644 src/solutions/request.rs create mode 100644 src/solutions/services/mod.rs create mode 100644 src/solutions/services/request.rs create mode 100644 src/solutions/staff_members/mod.rs create mode 100644 src/solutions/staff_members/request.rs diff --git a/src/client/graph.rs b/src/client/graph.rs index ba84a7f1..82d66a99 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -56,7 +56,7 @@ use crate::reports::ReportsApiClient; use crate::schema_extensions::{SchemaExtensionsApiClient, SchemaExtensionsIdApiClient}; use crate::service_principals::{ServicePrincipalsApiClient, ServicePrincipalsIdApiClient}; use crate::sites::{SitesApiClient, SitesIdApiClient}; -//use crate::solutions::SolutionsApiClient; +use crate::solutions::SolutionsApiClient; use crate::subscribed_skus::SubscribedSkusApiClient; use crate::subscriptions::{SubscriptionsApiClient, SubscriptionsIdApiClient}; use crate::teams::{TeamsApiClient, TeamsIdApiClient}; diff --git a/src/lib.rs b/src/lib.rs index 9fcb42d9..e02d1ea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ pub mod reports; pub mod schema_extensions; pub mod service_principals; pub mod sites; -//pub mod solutions; +pub mod solutions; pub mod subscribed_skus; pub mod subscriptions; pub mod teams; diff --git a/src/solutions/appointments/mod.rs b/src/solutions/appointments/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/appointments/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs new file mode 100644 index 00000000..908040a7 --- /dev/null +++ b/src/solutions/appointments/request.rs @@ -0,0 +1,53 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!( + AppointmentsApiClient, + AppointmentsIdApiClient, + ResourceIdentity::Appointments +); + +impl AppointmentsApiClient { + post!( + doc: "Create bookingAppointment", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "List appointments", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: appointments, + path: "/appointments/$count" + ); +} + +impl AppointmentsIdApiClient { + delete!( + doc: "Delete bookingAppointment", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get bookingAppointment", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update bookingAppointment", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); +} diff --git a/src/solutions/booking_businesses/mod.rs b/src/solutions/booking_businesses/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/booking_businesses/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs new file mode 100644 index 00000000..d3c4fc75 --- /dev/null +++ b/src/solutions/booking_businesses/request.rs @@ -0,0 +1,75 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +resource_api_client!( + BookingBusinessesApiClient, + BookingBusinessesIdApiClient, + ResourceIdentity::BookingBusinesses +); + +impl BookingBusinessesApiClient { + post!( + doc: "Create bookingBusiness", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "List bookingBusinesses", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: booking_businesses, + path: "/bookingBusinesses/$count" + ); +} + +impl BookingBusinessesIdApiClient { + api_client_link_id!(custom_question, CustomQuestionIdApiClient); + api_client_link_id!(appointment, AppointmentsIdApiClient); + api_client_link_id!(customer, CustomersIdApiClient); + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); + api_client_link!(services, ServicesApiClient); + api_client_link!(appointments, AppointmentsApiClient); + api_client_link_id!(service, ServicesIdApiClient); + api_client_link!(customers, CustomersApiClient); + api_client_link!(staff_members, StaffMembersApiClient); + + delete!( + doc: "Delete bookingBusiness", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusiness", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update bookingbusiness", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); +} diff --git a/src/solutions/custom_questions/mod.rs b/src/solutions/custom_questions/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/custom_questions/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs new file mode 100644 index 00000000..1a129677 --- /dev/null +++ b/src/solutions/custom_questions/request.rs @@ -0,0 +1,47 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!( + CustomQuestionsApiClient, + CustomQuestionsIdApiClient, + ResourceIdentity::CustomQuestions +); + +impl CustomQuestionsApiClient { + post!( + doc: "Create bookingCustomQuestion", + name: create_custom_questions, + path: "/customQuestions", + body: true + ); + get!( + doc: "List customQuestions", + name: list_custom_questions, + path: "/customQuestions" + ); + get!( + doc: "Get the number of the resource", + name: custom_questions, + path: "/customQuestions/$count" + ); +} + +impl CustomQuestionsIdApiClient { + delete!( + doc: "Delete bookingCustomQuestion", + name: delete_custom_questions, + path: "/customQuestions/{{RID}}" + ); + get!( + doc: "Get bookingCustomQuestion", + name: get_custom_questions, + path: "/customQuestions/{{RID}}" + ); + patch!( + doc: "Update bookingCustomQuestion", + name: update_custom_questions, + path: "/customQuestions/{{RID}}", + body: true + ); +} diff --git a/src/solutions/customers/mod.rs b/src/solutions/customers/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/customers/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs new file mode 100644 index 00000000..29a6019e --- /dev/null +++ b/src/solutions/customers/request.rs @@ -0,0 +1,47 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!( + CustomersApiClient, + CustomersIdApiClient, + ResourceIdentity::Customers +); + +impl CustomersApiClient { + post!( + doc: "Create bookingCustomer", + name: create_customers, + path: "/customers", + body: true + ); + get!( + doc: "List customers", + name: list_customers, + path: "/customers" + ); + get!( + doc: "Get the number of the resource", + name: customers, + path: "/customers/$count" + ); +} + +impl CustomersIdApiClient { + delete!( + doc: "Delete bookingCustomer", + name: delete_customers, + path: "/customers/{{RID}}" + ); + get!( + doc: "Get bookingCustomer", + name: get_customers, + path: "/customers/{{RID}}" + ); + patch!( + doc: "Update bookingCustomer", + name: update_customers, + path: "/customers/{{RID}}", + body: true + ); +} diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs new file mode 100644 index 00000000..b4a42f77 --- /dev/null +++ b/src/solutions/mod.rs @@ -0,0 +1,15 @@ +mod appointments; +mod booking_businesses; +mod custom_questions; +mod customers; +mod request; +mod services; +mod staff_members; + +pub use appointments::*; +pub use booking_businesses::*; +pub use custom_questions::*; +pub use customers::*; +pub use request::*; +pub use services::*; +pub use staff_members::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs new file mode 100644 index 00000000..8a3cfc46 --- /dev/null +++ b/src/solutions/request.rs @@ -0,0 +1,23 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); + +impl SolutionsApiClient { + api_client_link_id!(booking_business, BookingBusinessesIdApiClient); + api_client_link!(booking_businesses, BookingBusinessesApiClient); + + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); +} diff --git a/src/solutions/services/mod.rs b/src/solutions/services/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/services/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs new file mode 100644 index 00000000..c897619b --- /dev/null +++ b/src/solutions/services/request.rs @@ -0,0 +1,47 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!( + ServicesApiClient, + ServicesIdApiClient, + ResourceIdentity::Services +); + +impl ServicesApiClient { + post!( + doc: "Create bookingService", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "List services", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: services, + path: "/services/$count" + ); +} + +impl ServicesIdApiClient { + delete!( + doc: "Delete bookingService", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get bookingService", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update bookingservice", + name: update_services, + path: "/services/{{RID}}", + body: true + ); +} diff --git a/src/solutions/staff_members/mod.rs b/src/solutions/staff_members/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/staff_members/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs new file mode 100644 index 00000000..e3310d61 --- /dev/null +++ b/src/solutions/staff_members/request.rs @@ -0,0 +1,47 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +resource_api_client!( + StaffMembersApiClient, + StaffMembersIdApiClient, + ResourceIdentity::StaffMembers +); + +impl StaffMembersApiClient { + post!( + doc: "Create bookingStaffMember", + name: create_staff_members, + path: "/staffMembers", + body: true + ); + get!( + doc: "List staffMembers", + name: list_staff_members, + path: "/staffMembers" + ); + get!( + doc: "Get the number of the resource", + name: staff_members, + path: "/staffMembers/$count" + ); +} + +impl StaffMembersIdApiClient { + delete!( + doc: "Delete bookingStaffMember", + name: delete_staff_members, + path: "/staffMembers/{{RID}}" + ); + get!( + doc: "Get bookingStaffMember", + name: get_staff_members, + path: "/staffMembers/{{RID}}" + ); + patch!( + doc: "Update bookingstaffmember", + name: update_staff_members, + path: "/staffMembers/{{RID}}", + body: true + ); +} From 599fb4b995efa2adf75f49fe0beaa57fd4fe2498 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 8 Mar 2024 07:01:16 +0000 Subject: [PATCH 094/118] fix: typo in Custom Questions API link --- graph-codegen/src/settings/resource_settings.rs | 2 +- src/client/graph.rs | 2 +- src/solutions/booking_businesses/request.rs | 14 +++++++------- src/solutions/request.rs | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index d3197c14..fb96d3c0 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1207,7 +1207,7 @@ impl ResourceSettings { ApiClientLink::Struct("services", "ServicesApiClient"), ApiClientLink::StructId("service", "ServicesIdApiClient"), ApiClientLink::Struct("custom_questions", "CustomQuestionsApiClient"), - ApiClientLink::StructId("custom_question", "CustomQuestionIdApiClient"), + ApiClientLink::StructId("custom_question", "CustomQuestionsIdApiClient"), ApiClientLink::Struct("customers", "CustomersApiClient"), ApiClientLink::StructId("customer", "CustomersIdApiClient"), ApiClientLink::Struct("staff_members", "StaffMembersApiClient"), diff --git a/src/client/graph.rs b/src/client/graph.rs index 82d66a99..2e409d14 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -379,7 +379,7 @@ impl Graph { api_client_impl!(sites, SitesApiClient, site, SitesIdApiClient); - //api_client_impl!(solutions, SolutionsApiClient); + api_client_impl!(solutions, SolutionsApiClient); api_client_impl!( subscribed_skus, diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index d3c4fc75..b400b041 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -29,16 +29,16 @@ impl BookingBusinessesApiClient { } impl BookingBusinessesIdApiClient { - api_client_link_id!(custom_question, CustomQuestionIdApiClient); - api_client_link_id!(appointment, AppointmentsIdApiClient); - api_client_link_id!(customer, CustomersIdApiClient); - api_client_link_id!(staff_member, StaffMembersIdApiClient); - api_client_link!(custom_questions, CustomQuestionsApiClient); - api_client_link!(services, ServicesApiClient); api_client_link!(appointments, AppointmentsApiClient); api_client_link_id!(service, ServicesIdApiClient); - api_client_link!(customers, CustomersApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); + api_client_link!(services, ServicesApiClient); + api_client_link_id!(custom_question, CustomQuestionsIdApiClient); api_client_link!(staff_members, StaffMembersApiClient); + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link!(customers, CustomersApiClient); + api_client_link_id!(customer, CustomersIdApiClient); + api_client_link_id!(appointment, AppointmentsIdApiClient); delete!( doc: "Delete bookingBusiness", diff --git a/src/solutions/request.rs b/src/solutions/request.rs index 8a3cfc46..5e4314c0 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -6,8 +6,8 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); impl SolutionsApiClient { - api_client_link_id!(booking_business, BookingBusinessesIdApiClient); api_client_link!(booking_businesses, BookingBusinessesApiClient); + api_client_link_id!(booking_business, BookingBusinessesIdApiClient); get!( doc: "Get solutions", From a60c75b49a95dcb630e047f59a9f9ab65011484b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 22 Mar 2024 02:19:55 -0400 Subject: [PATCH 095/118] Clean up OpenId methods that will be slated for later release --- examples/interactive_auth/auth_code.rs | 1 + examples/interactive_auth/openid.rs | 11 + graph-core/src/cache/token_cache.rs | 6 +- graph-core/src/identity/client_application.rs | 5 - graph-http/src/client.rs | 6 +- .../confidential_client_application.rs | 18 +- .../credentials/open_id_authorization_url.rs | 19 +- .../credentials/open_id_credential.rs | 302 +----------------- graph-oauth/src/identity/id_token.rs | 4 +- src/client/graph.rs | 6 +- 10 files changed, 30 insertions(+), 348 deletions(-) diff --git a/examples/interactive_auth/auth_code.rs b/examples/interactive_auth/auth_code.rs index f7288566..440d46e7 100644 --- a/examples/interactive_auth/auth_code.rs +++ b/examples/interactive_auth/auth_code.rs @@ -41,6 +41,7 @@ async fn authenticate( .with_tenant(tenant_id) .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token. .with_redirect_uri(Url::parse(redirect_uri)?) + // Can be Secret("value"), Assertion("value"), or X509Certificate .with_interactive_auth(Secret("secret".to_string()), Default::default()) .into_credential_builder()?; diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index 1f430533..68698a25 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -7,6 +7,17 @@ use graph_rs_sdk::{ GraphClient, }; +// Use the into_credential_builder method to map the WebViewAuthorizationEvent to a +// CredentialBuilder result. The CredentialBuilder for openid will be the OpenIdCredentialBuilder. +// The into_credential_builder method transforms WebViewAuthorizationEvent::Authorized to a +// successful result. +// +// A WebViewAuthorizationEvent::Unauthorized and WebViewAuthorizationEvent::WindowClosed +// are returned as errors in the result: Result<(AuthorizationResponse, CredentialBuilder), WebViewError> +// +// The openid_authenticate2 method shows handling the WebViewAuthorizationEvent manually which is the +// default return type of using with_interactive_auth and provides better event handling. + async fn openid_authenticate( tenant_id: &str, client_id: &str, diff --git a/graph-core/src/cache/token_cache.rs b/graph-core/src/cache/token_cache.rs index 5655d74a..f348750a 100644 --- a/graph-core/src/cache/token_cache.rs +++ b/graph-core/src/cache/token_cache.rs @@ -1,4 +1,4 @@ -use crate::identity::{DecodedJwt, ForceTokenRefresh}; +use crate::identity::ForceTokenRefresh; use async_trait::async_trait; use graph_error::AuthExecutionError; @@ -27,8 +27,4 @@ pub trait TokenCache { async fn get_token_silent_async(&mut self) -> Result<Self::Token, AuthExecutionError>; fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); - - fn decoded_jwt(&self) -> Option<&DecodedJwt> { - None - } } diff --git a/graph-core/src/identity/client_application.rs b/graph-core/src/identity/client_application.rs index bb740099..506174d7 100644 --- a/graph-core/src/identity/client_application.rs +++ b/graph-core/src/identity/client_application.rs @@ -1,4 +1,3 @@ -use crate::identity::DecodedJwt; use async_trait::async_trait; use dyn_clone::DynClone; use graph_error::AuthExecutionResult; @@ -27,10 +26,6 @@ pub trait ClientApplication: DynClone + Send + Sync { async fn get_token_silent_async(&mut self) -> AuthExecutionResult<String>; fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh); - - fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { - None - } } #[async_trait] diff --git a/graph-http/src/client.rs b/graph-http/src/client.rs index 12d54d11..30e2e72b 100644 --- a/graph-http/src/client.rs +++ b/graph-http/src/client.rs @@ -1,5 +1,5 @@ use crate::blocking::BlockingClient; -use graph_core::identity::{ClientApplication, DecodedJwt, ForceTokenRefresh}; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT}; use reqwest::redirect::Policy; use reqwest::tls::Version; @@ -269,10 +269,6 @@ impl Client { self.client_application .with_force_token_refresh(force_token_refresh); } - - pub fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { - self.client_application.get_decoded_jwt() - } } impl Default for Client { diff --git a/graph-oauth/src/identity/credentials/confidential_client_application.rs b/graph-oauth/src/identity/credentials/confidential_client_application.rs index df3bc437..4f6099c6 100644 --- a/graph-oauth/src/identity/credentials/confidential_client_application.rs +++ b/graph-oauth/src/identity/credentials/confidential_client_application.rs @@ -2,17 +2,13 @@ use std::collections::HashMap; use std::fmt::Debug; use async_trait::async_trait; -use jsonwebtoken::TokenData; use reqwest::Response; use url::Url; use uuid::Uuid; -use graph_core::identity::{ClientApplication, DecodedJwt, ForceTokenRefresh}; -use graph_core::{ - cache::{AsBearer, TokenCache}, - identity::Claims, -}; +use graph_core::cache::{AsBearer, TokenCache}; +use graph_core::identity::{ClientApplication, ForceTokenRefresh}; use graph_error::{AuthExecutionResult, IdentityResult}; use crate::identity::{ @@ -79,10 +75,6 @@ impl<Credential: Clone + Debug + Send + Sync + TokenCache + TokenCredentialExecu self.credential .with_force_token_refresh(force_token_refresh); } - - fn get_decoded_jwt(&self) -> Option<&DecodedJwt> { - self.credential.decoded_jwt() - } } #[async_trait] @@ -176,12 +168,6 @@ impl From<OpenIdCredential> for ConfidentialClientApplication<OpenIdCredential> } } -impl ConfidentialClientApplication<OpenIdCredential> { - pub fn decoded_id_token(&self) -> Option<TokenData<Claims>> { - self.credential.get_decoded_jwt() - } -} - #[cfg(test)] mod test { use crate::identity::Authority; diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 8fbdbfb6..6548e2e5 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -116,7 +116,6 @@ pub struct OpenIdAuthorizationUrlParameters { /// this parameter during re-authentication, after already extracting the login_hint /// optional claim from an earlier sign-in. pub(crate) login_hint: Option<String>, - verify_id_token: bool, } impl Debug for OpenIdAuthorizationUrlParameters { @@ -154,7 +153,6 @@ impl OpenIdAuthorizationUrlParameters { prompt: Default::default(), domain_hint: None, login_hint: None, - verify_id_token: Default::default(), }) } @@ -168,7 +166,6 @@ impl OpenIdAuthorizationUrlParameters { prompt: Default::default(), domain_hint: None, login_hint: None, - verify_id_token: Default::default(), } } @@ -177,11 +174,7 @@ impl OpenIdAuthorizationUrlParameters { } pub fn into_credential(self, authorization_code: impl AsRef<str>) -> OpenIdCredentialBuilder { - OpenIdCredentialBuilder::new_with_auth_code( - self.app_config, - authorization_code, - self.verify_id_token, - ) + OpenIdCredentialBuilder::new_with_auth_code(self.app_config, authorization_code) } pub fn url(&self) -> IdentityResult<Url> { @@ -273,10 +266,6 @@ impl OpenIdAuthorizationUrlParameters { credential_builder.with_client_secret(client_secret); - if self.verify_id_token { - credential_builder.with_id_token_verification(true); - } - Ok(WebViewAuthorizationEvent::Authorized { authorization_response, credential_builder, @@ -570,11 +559,6 @@ impl OpenIdAuthorizationUrlParameterBuilder { self } - pub fn with_id_token_verification(&mut self, verify_id_token: bool) -> &mut Self { - self.credential.verify_id_token = verify_id_token; - self - } - #[cfg(feature = "interactive-auth")] pub fn with_interactive_auth( &self, @@ -601,7 +585,6 @@ impl OpenIdAuthorizationUrlParameterBuilder { OpenIdCredentialBuilder::new_with_auth_code( self.credential.app_config.clone(), authorization_code, - false, ) } } diff --git a/graph-oauth/src/identity/credentials/open_id_credential.rs b/graph-oauth/src/identity/credentials/open_id_credential.rs index ca0b06fe..ded53ae1 100644 --- a/graph-oauth/src/identity/credentials/open_id_credential.rs +++ b/graph-oauth/src/identity/credentials/open_id_credential.rs @@ -5,26 +5,23 @@ use async_trait::async_trait; use graph_core::cache::{CacheStore, InMemoryCacheStore, TokenCache}; use http::{HeaderMap, HeaderName, HeaderValue}; -use jsonwebtoken::TokenData; use reqwest::IntoUrl; -use url::{ParseError, Url}; +use url::Url; use uuid::Uuid; use graph_core::{ crypto::{GenPkce, ProofKeyCodeExchange}, http::{AsyncResponseConverterExt, ResponseConverterExt}, - identity::{Claims, DecodedJwt, ForceTokenRefresh, JwksKeySet}, + identity::ForceTokenRefresh, }; -use graph_error::{ - AuthExecutionError, AuthExecutionResult, AuthorizationFailure, IdentityResult, AF, -}; +use graph_error::{AuthExecutionError, AuthExecutionResult, IdentityResult, AF}; use crate::identity::credentials::app_config::{AppConfig, AppConfigBuilder}; use crate::identity::{ - tracing_targets::CREDENTIAL_EXECUTOR, Authority, AuthorizationResponse, AzureCloudInstance, - ConfidentialClientApplication, IdToken, OpenIdAuthorizationUrlParameterBuilder, - OpenIdAuthorizationUrlParameters, Token, TokenCredentialExecutor, + Authority, AuthorizationResponse, AzureCloudInstance, ConfidentialClientApplication, IdToken, + OpenIdAuthorizationUrlParameterBuilder, OpenIdAuthorizationUrlParameters, Token, + TokenCredentialExecutor, }; use crate::internal::{AuthParameter, AuthSerializer}; @@ -68,8 +65,6 @@ pub struct OpenIdCredential { pub(crate) pkce: Option<ProofKeyCodeExchange>, serializer: AuthSerializer, token_cache: InMemoryCacheStore<Token>, - verify_id_token: bool, - id_token_jwt: Option<DecodedJwt>, } impl Debug for OpenIdCredential { @@ -100,8 +95,6 @@ impl OpenIdCredential { pkce: None, serializer: Default::default(), token_cache: Default::default(), - verify_id_token: Default::default(), - id_token_jwt: None, }) } @@ -126,191 +119,6 @@ impl OpenIdCredential { self.pkce.as_ref() } - pub(crate) fn get_decoded_jwt(&self) -> Option<TokenData<Claims>> { - self.id_token_jwt.clone() - } - - pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { - let uri = self - .app_config - .azure_cloud_instance - .openid_configuration_uri(&self.app_config.authority) - .map_err(AuthorizationFailure::from)?; - Ok(reqwest::blocking::get(uri)?) - } - - pub async fn get_openid_config_async(&self) -> AuthExecutionResult<reqwest::Response> { - let uri = self - .app_config - .azure_cloud_instance - .openid_configuration_uri(&self.app_config.authority) - .map_err(AuthorizationFailure::from)?; - reqwest::get(uri).await.map_err(AuthExecutionError::from) - } - - pub fn get_jwks(&self) -> AuthExecutionResult<reqwest::blocking::Response> { - let config_response = self.get_openid_config()?; - let json: serde_json::Value = config_response.json()?; - let jwks_uri = json["jwks_uri"] - .as_str() - .ok_or(AuthExecutionError::Authorization(AF::msg_err( - "jwks_uri", - "not found in openid configuration", - )))?; - Ok(reqwest::blocking::get(jwks_uri)?) - } - - pub async fn get_jwks_async(&self) -> AuthExecutionResult<reqwest::Response> { - let config_response = self.get_openid_config_async().await?; - let json: serde_json::Value = config_response.json().await?; - let jwks_uri = json["jwks_uri"] - .as_str() - .ok_or(AuthExecutionError::Authorization(AF::msg_err( - "jwks_uri", - "not found in openid configuration", - )))?; - reqwest::get(jwks_uri) - .await - .map_err(AuthExecutionError::from) - } - - pub fn verify_jwks(&self) -> AuthExecutionResult<TokenData<Claims>> { - let cache_id = self.app_config.cache_id.to_string(); - let token = self - .token_cache - .get(cache_id.as_str()) - .ok_or(AF::msg_err("token", "no cached token"))?; - let mut id_token = token - .id_token - .ok_or(AF::msg_err("id_token", "no cached id_token"))?; - self.verify_jwks_from_token(&mut id_token) - } - - pub async fn verify_jwks_async(&self) -> AuthExecutionResult<TokenData<Claims>> { - let cache_id = self.app_config.cache_id.to_string(); - let token = self - .token_cache - .get(cache_id.as_str()) - .ok_or(AF::msg_err("token", "no cached token"))?; - let mut id_token = token - .id_token - .clone() - .ok_or(AF::msg_err("id_token", "no cached id_token"))?; - self.verify_jwks_from_token_async(&mut id_token).await - } - - fn verify_jwks_from_token( - &self, - id_token: &mut IdToken, - ) -> AuthExecutionResult<TokenData<Claims>> { - let headers = id_token.decode_header()?; - let kid = headers - .kid - .as_ref() - .ok_or(AF::msg_err("id_token", "id_token header does not have kid"))?; - - let response = self.get_jwks()?; - let status = response.status(); - - tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks key set response received; status={status:#?}"); - - let key_set: JwksKeySet = response.json()?; - let jwks_key = key_set - .keys - .iter() - .find(|key| key.kid.eq(kid)) - .cloned() - .ok_or(AF::msg_err( - "kid", - "no match found for kid in json web keys", - )) - .map_err(AuthExecutionError::from)?; - - tracing::debug!(target: CREDENTIAL_EXECUTOR, "found matching kid in jwks key set"); - - if self.app_config.tenant_id.is_some() { - Ok(id_token.decode( - jwks_key.modulus.as_str(), - jwks_key.exponent.as_str(), - &self.app_config.client_id.to_string(), - Some(self.issuer().map_err(AuthorizationFailure::from)?.as_str()), - )?) - } else { - Ok(id_token.decode( - jwks_key.modulus.as_str(), - jwks_key.exponent.as_str(), - &self.app_config.client_id.to_string(), - None, - )?) - } - } - - async fn verify_jwks_from_token_async( - &self, - id_token: &mut IdToken, - ) -> AuthExecutionResult<TokenData<Claims>> { - let headers = id_token.decode_header()?; - let value2 = serde_json::to_string(&headers).unwrap(); - tracing::debug!( - target: CREDENTIAL_EXECUTOR, - value2 - ); - - let kid = headers - .kid - .as_ref() - .ok_or(AF::msg_err("id_token", "id_token header does not have kid"))?; - - let response = self.get_jwks_async().await?; - let key_set: JwksKeySet = response.json().await?; - let jwks_key = key_set - .keys - .iter() - .find(|key| key.kid.eq(kid)) - .cloned() - .ok_or(AF::msg_err( - "kid", - "no match found for kid in json web keys", - )) - .map_err(AuthExecutionError::from)?; - - if self.app_config.tenant_id.is_some() { - Ok(id_token.decode( - jwks_key.modulus.as_str(), - jwks_key.exponent.as_str(), - &self.app_config.client_id.to_string(), - Some(self.issuer().map_err(AuthorizationFailure::from)?.as_str()), - )?) - } else { - Ok(id_token.decode( - jwks_key.modulus.as_str(), - jwks_key.exponent.as_str(), - &self.app_config.client_id.to_string(), - None, - )?) - } - } - - #[allow(unused)] - async fn verify_authorization_id_token_async( - &mut self, - ) -> Option<AuthExecutionResult<TokenData<Claims>>> { - if let Some(id_token) = self.app_config.id_token.as_ref() { - let mut id_token_clone = id_token.clone(); - if !id_token_clone.verified { - return match self.verify_jwks_from_token_async(&mut id_token_clone).await { - Ok(token_data) => { - self.app_config.with_id_token(id_token_clone); - Some(Ok(token_data)) - } - Err(err) => Some(Err(err)), - }; - // return Some(self.verify_jwks_from_token_async(&mut id_token_clone).await) - } - } - None - } - fn execute_cached_token_refresh(&mut self, cache_id: String) -> AuthExecutionResult<Token> { let response = self.execute()?; @@ -321,27 +129,6 @@ impl OpenIdCredential { } let new_token: Token = response.json()?; - - if self.verify_id_token { - if let Some(mut id_token) = new_token.id_token.clone() { - tracing::debug!(target: CREDENTIAL_EXECUTOR, "performing jwks verification"); - - let id_token_verification_result = self.verify_jwks_from_token(&mut id_token); - if let Ok(token_data) = id_token_verification_result { - self.id_token_jwt = Some(token_data); - dbg!(&self.id_token_jwt); - tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); - } else if let Err(err) = id_token_verification_result { - tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification failed - evicting token from cache"); - - // The new token has not been stored in the cache but we still need evict any previous tokens. - self.refresh_token = None; - self.token_cache.evict(cache_id.as_str()); - return Err(err); - } - } - } - self.token_cache.store(cache_id, new_token.clone()); if new_token.refresh_token.is_some() { @@ -365,37 +152,11 @@ impl OpenIdCredential { let new_token: Token = response.json().await?; - if self.verify_id_token { - if let Some(mut id_token) = new_token.id_token.clone() { - tracing::debug!( - target: CREDENTIAL_EXECUTOR, - verify_id_token = self.verify_id_token, - "performing jwks verification:" - ); - - let id_token_verification_result = - self.verify_jwks_from_token_async(&mut id_token).await; - if let Ok(token_data) = id_token_verification_result { - self.id_token_jwt = Some(token_data); - dbg!(&self.id_token_jwt); - tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification successful"); - } else if let Err(err) = id_token_verification_result { - tracing::debug!(target: CREDENTIAL_EXECUTOR, "jwks verification failed - evicting token from cache"); - - // The new token has not been stored in the cache but we still need evict any previous tokens. - self.refresh_token = None; - self.token_cache.evict(cache_id.as_str()); - return Err(err); - } - } - } - - self.token_cache.store(cache_id, new_token.clone()); - if new_token.refresh_token.is_some() { self.refresh_token = new_token.refresh_token.clone(); } + self.token_cache.store(cache_id, new_token.clone()); Ok(new_token) } } @@ -484,10 +245,6 @@ impl TokenCache for OpenIdCredential { fn with_force_token_refresh(&mut self, force_token_refresh: ForceTokenRefresh) { self.app_config.force_token_refresh = force_token_refresh; } - - fn decoded_jwt(&self) -> Option<&DecodedJwt> { - self.id_token_jwt.as_ref() - } } #[async_trait] @@ -622,8 +379,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - verify_id_token: Default::default(), - id_token_jwt: None, }, } } @@ -640,8 +395,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - verify_id_token: Default::default(), - id_token_jwt: None, }, } } @@ -649,7 +402,6 @@ impl OpenIdCredentialBuilder { pub(crate) fn new_with_auth_code( mut app_config: AppConfig, authorization_code: impl AsRef<str>, - verify_id_token: bool, ) -> OpenIdCredentialBuilder { app_config.scope.insert("openid".to_string()); OpenIdCredentialBuilder { @@ -662,8 +414,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - verify_id_token, - id_token_jwt: None, }, } } @@ -684,8 +434,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache: Default::default(), - verify_id_token: Default::default(), - id_token_jwt: None, }, } } @@ -705,8 +453,6 @@ impl OpenIdCredentialBuilder { pkce: None, serializer: Default::default(), token_cache, - verify_id_token: Default::default(), - id_token_jwt: None, }, } } @@ -751,36 +497,6 @@ impl OpenIdCredentialBuilder { Ok(self) } - pub fn with_id_token_verification(&mut self, verify_id_token: bool) -> &mut Self { - self.credential.verify_id_token = verify_id_token; - self - } - - pub fn issuer(&self) -> Result<Url, ParseError> { - self.credential.issuer() - } - - pub fn get_openid_config(&self) -> AuthExecutionResult<reqwest::blocking::Response> { - self.credential.get_openid_config() - } - - pub async fn get_openid_config_async(&self) -> AuthExecutionResult<reqwest::Response> { - self.credential.get_openid_config_async().await - } - - pub fn get_jwks(&self) -> AuthExecutionResult<reqwest::blocking::Response> { - self.credential.get_jwks() - } - - pub async fn get_jwks_async(&self) -> AuthExecutionResult<reqwest::Response> { - self.credential.get_jwks_async().await - } - - #[allow(dead_code)] - pub(crate) async fn verify_jwks_async(&self) -> AuthExecutionResult<TokenData<Claims>> { - self.credential.verify_jwks_async().await - } - pub fn credential(&self) -> &OpenIdCredential { &self.credential } @@ -809,9 +525,9 @@ impl From<(AppConfig, AuthorizationResponse)> for OpenIdCredentialBuilder { Some(authorization_code.as_ref()), None, )); - OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code, true) + OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code) } else { - OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code, false) + OpenIdCredentialBuilder::new_with_auth_code(app_config, authorization_code) } } else { OpenIdCredentialBuilder::new_with_token( diff --git a/graph-oauth/src/identity/id_token.rs b/graph-oauth/src/identity/id_token.rs index 0bd67fab..9678805b 100644 --- a/graph-oauth/src/identity/id_token.rs +++ b/graph-oauth/src/identity/id_token.rs @@ -87,12 +87,14 @@ impl IdToken { jsonwebtoken::decode_header(self.id_token.as_str()) } + /// Slated Post 2.0 Release /// Decode and verify the id token using the following parameters: /// modulus (n): product of two prime numbers used to generate key pair. /// Exponent (e): exponent used to decode the data. /// client_id: tenant client id in Azure. /// issuer: issuer for tenant in Azure. - pub fn decode( + #[allow(dead_code)] + fn decode( &mut self, modulus: &str, exponent: &str, diff --git a/src/client/graph.rs b/src/client/graph.rs index bb4a6384..54d08462 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -68,7 +68,7 @@ use crate::teams_templates::{TeamsTemplatesApiClient, TeamsTemplatesIdApiClient} use crate::teamwork::TeamworkApiClient; use crate::users::{UsersApiClient, UsersIdApiClient}; use crate::{GRAPH_URL, GRAPH_URL_BETA}; -use graph_core::identity::{DecodedJwt, ForceTokenRefresh}; +use graph_core::identity::ForceTokenRefresh; use lazy_static::lazy_static; lazy_static! { @@ -298,10 +298,6 @@ impl GraphClient { self.endpoint = url.clone(); } - pub fn decoded_jwt(&self) -> Option<&DecodedJwt> { - self.client.get_decoded_jwt() - } - api_client_impl!(admin, AdminApiClient); api_client_impl!(app_catalogs, AppCatalogsApiClient); From 8425585c856bb1d8966b48464fd946c79fe80269 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Fri, 22 Mar 2024 02:36:36 -0400 Subject: [PATCH 096/118] Update README --- README.md | 84 ++++++++++++++++++------------------------------------- 1 file changed, 27 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 779b64fa..f5a6230e 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,29 @@ # graph-rs-sdk  -[](https://crates.io/crates/graph-rs-sdk) -[](https://crates.io/crates/graph-rs-sdk/2.0.0-beta.0) - - +[](https://crates.io/crates/graph-rs-sdk) ### Rust SDK Client for Microsoft Graph and Microsoft Identity Platform -### Available on [crates.io](https://crates.io/crates/graph-rs-sdk/1.1.4) - v1.1.4 - Latest Stable Version +### Available on [crates.io](https://crates.io/crates/graph-rs-sdk/1.1.4) - v2.0.0 - Latest Stable Version -Features: +#### Features: -- Microsoft Graph V1 and Beta API Client +Microsoft Graph V1 and Beta API Client + - Wide support for Graph APIs - Paging using Streaming, Channels, or Iterators - Upload Sessions, OData Queries, and File Downloads -- Microsoft Graph Identity Platform OAuth2 and OpenId Connect Client - - Auth Code, Client Credentials, Device Code, OpenId - - X509 Certificates, PKCE - - Interactive Authentication - - Automatic Token Refresh + +Microsoft Identity Platform (Getting Access Tokens) +- Auth Code, Client Credentials, Device Code, OpenId +- In Memory Token Cache +- Automatic Token Refresh +- Interactive WebView Auth (feature = `interactive-auth`) +- X509 Certificate (feature = `openssl`) and Proof Key Code Exchange (PKCE) Support + ```toml -graph-rs-sdk = "1.1.4" +graph-rs-sdk = "2.0.0" tokio = { version = "1.25.0", features = ["full"] } ``` @@ -46,31 +47,6 @@ use futures::StreamExt; use graph_rs_sdk::*; ``` -### Pre Release Version (May Be Unstable) - -[](https://crates.io/crates/graph-rs-sdk/2.0.0-beta.0) - -- Complete rewrite of SDK Client for the Microsoft Identity Platform -- In Memory Token Cache -- Automatic Token Refresh -- Interactive Auth Using WebView -- X509 Certificate Support - -See https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0 for examples and docs. - -On **Pre-Release** Only: -- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) - - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) - - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) - - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/client_credentials)) -- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/authorization_sign_in) -- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth)) -- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/certificate_auth) - -``` -graph-rs-sdk = "2.0.0-beta.0" -``` - Contributing and Wiki: - [Contributions](https://github.com/sreeise/graph-rs-sdk/wiki/Contributing) - [Wiki](https://github.com/sreeise/graph-rs-sdk/wiki) @@ -118,6 +94,15 @@ OAuth and Openid * [Automatic Token Refresh](#automatic-token-refresh) * [Interactive Authentication](#interactive-authentication) + +[Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) +- [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) +- [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) +- [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/client_credentials)) +- [Url Builders For Flows Using Sign In To Get Authorization Code - Build Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/authorization_sign_in) +- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth)) +- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/certificate_auth) + ### What APIs are available The APIs available are generated from OpenApi configs that are stored in Microsoft's msgraph-metadata repository @@ -134,11 +119,7 @@ The crate can do both an async and blocking requests. #### Async Client (default) -<<<<<<< HEAD - graph-rs-sdk = "2.0.0-beta.0" -======= - graph-rs-sdk = "1.1.4" ->>>>>>> master + graph-rs-sdk = "2.0.0" tokio = { version = "1.25.0", features = ["full"] } #### Example @@ -170,11 +151,7 @@ async fn main() -> GraphResult<()> { To use the blocking client use the `into_blocking()` method. You should not use `tokio` when using the blocking client. -<<<<<<< HEAD - graph-rs-sdk = "2.0.0-beta.0" -======= - graph-rs-sdk = "1.1.4" ->>>>>>> master + graph-rs-sdk = "2.0.0" #### Example use graph_rs_sdk::*; @@ -1037,12 +1014,6 @@ async fn get_user() -> GraphResult<()> { ## OAuth - Getting Access Tokens - -### Warning -This crate is undergoing major development in order to support all or most scenarios in the -Microsoft Identity Platform where its possible to do so. The master branch on GitHub may have some -unstable features. Any version that is not a pre-release version of the crate is considered stable. - Use application builders to store your auth configuration and have the client handle the access token requests for you. @@ -1056,7 +1027,6 @@ Support for: #### Detailed Examples: - - [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) @@ -1305,11 +1275,11 @@ async fn build_client( ### Interactive Authentication -Requires Feature `interactive_auth` +Requires Feature `interactive-auth` ```toml [dependencies] -graph-rs-sdk = { version = "...", features = ["interactive_auth"] } +graph-rs-sdk = { version = "...", features = ["interactive-auth"] } ``` Interactive Authentication uses the [wry](https://github.com/tauri-apps/wry) crate to run web view on From c0f049df3aa966579574860473e706cab6c1eebb Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 20 Apr 2024 07:01:34 -0400 Subject: [PATCH 097/118] Update interactive auth to call run_return to prevent main thread from being destroyed --- README.md | 2 + .../identity_platform_auth/device_code.rs | 24 +++++----- examples/interactive_auth/device_code.rs | 46 +++++++++++++++++++ examples/interactive_auth/main.rs | 1 + examples/interactive_auth/openid.rs | 5 +- graph-error/src/authorization_failure.rs | 18 +++++++- .../credentials/device_code_credential.rs | 45 ++++++++++-------- .../credentials/open_id_authorization_url.rs | 5 +- .../src/interactive/interactive_auth.rs | 6 ++- 9 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 examples/interactive_auth/device_code.rs diff --git a/README.md b/README.md index f5a6230e..3683a49c 100644 --- a/README.md +++ b/README.md @@ -1277,6 +1277,8 @@ async fn build_client( Requires Feature `interactive-auth` +NOTE: Device code interactive auth does not currently work in async code. + ```toml [dependencies] graph-rs-sdk = { version = "...", features = ["interactive-auth"] } diff --git a/examples/identity_platform_auth/device_code.rs b/examples/identity_platform_auth/device_code.rs index 959b7f08..bbef4768 100644 --- a/examples/identity_platform_auth/device_code.rs +++ b/examples/identity_platform_auth/device_code.rs @@ -9,20 +9,18 @@ use warp::hyper::body::HttpBody; // https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code -static CLIENT_ID: &str = "<CLIENT_ID>"; -static TENANT: &str = "<TENANT>"; - // Make the call to get a device code from the user. // Poll the device code endpoint to get the code and a url that the user must // go to in order to enter the code. Polling will continue until either the user -// has entered the code. Once a successful code has been entered the next time the -// device code endpoint is polled an access token is returned. -fn poll_device_code() -> anyhow::Result<()> { - let mut device_executor = PublicClientApplication::builder(CLIENT_ID) +// has entered the code or a fatal error has occurred which causes polling to cease. +// Once a successful code has been entered the next time the device code endpoint +// is polled an access token is returned. +fn poll_device_code(client_id: &str, tenant: &str, scope: Vec<&str>) -> anyhow::Result<()> { + let mut device_executor = PublicClientApplication::builder(client_id) .with_device_code_executor() - .with_scope(vec!["User.Read"]) - .with_tenant(TENANT) + .with_scope(scope) + .with_tenant(tenant) .poll()?; while let Ok(response) = device_executor.recv() { @@ -32,11 +30,11 @@ fn poll_device_code() -> anyhow::Result<()> { Ok(()) } -fn get_token(device_code: &str) { - let mut public_client = PublicClientApplication::builder(CLIENT_ID) +fn get_token(device_code: &str, client_id: &str, tenant: &str, scope: Vec<&str>) { + let mut public_client = PublicClientApplication::builder(client_id) .with_device_code(device_code) - .with_scope(["User.Read"]) - .with_tenant(TENANT) + .with_scope(scope) + .with_tenant(tenant) .build(); let response = public_client.execute().unwrap(); diff --git a/examples/interactive_auth/device_code.rs b/examples/interactive_auth/device_code.rs new file mode 100644 index 00000000..ba85204f --- /dev/null +++ b/examples/interactive_auth/device_code.rs @@ -0,0 +1,46 @@ +use graph_oauth::interactive::WebViewOptions; +use graph_oauth::PublicClientApplication; +use graph_rs_sdk::GraphClient; + +// NOTE: Device code interactive auth does not work in async code. + +// Device code interactive auth returns a polling executor in order to get the +// public client credential which you can pass to the GraphClient. + +// The DeviceAuthorizationResponse returns the initial JSON response body +// that contains the device code that the user enters when logging in. + +/// Example run: +/// ```rust,ignore +/// fn main() { +/// std::env::set_var("RUST_LOG", "debug"); +/// pretty_env_logger::init(); +/// let graph_client = auth("client-id", "tenant-id", vec!["User.Read"]).unwrap(); +/// log::debug!("{:#?}", &graph_client); +/// } +/// ``` +fn device_code_authenticate( + client_id: &str, + tenant: &str, + scope: Vec<&str>, +) -> anyhow::Result<GraphClient> { + let (device_authorization_response, mut interactive_auth_executor) = + PublicClientApplication::builder(client_id) + .with_tenant(tenant) + .with_scope(scope) + .with_device_code_executor() + .with_interactive_auth(WebViewOptions::default())?; + + log::debug!("{:#?}", device_authorization_response); + log::debug!( + "To sign in, enter the code {:#?} to authenticate.", + device_authorization_response.user_code + ); + + // After providing the code to the user to sign in run the executor `poll` method which + // will poll for a response to the authentication and if successful return a + // PublicClientApplication<DeviceCodeCredential> + let public_client = interactive_auth_executor.poll()?; + + Ok(GraphClient::from(&public_client)) +} diff --git a/examples/interactive_auth/main.rs b/examples/interactive_auth/main.rs index 081dcd5c..ccad8650 100644 --- a/examples/interactive_auth/main.rs +++ b/examples/interactive_auth/main.rs @@ -4,6 +4,7 @@ extern crate pretty_env_logger; #[macro_use] extern crate log; mod auth_code; +mod device_code; mod openid; mod webview_options; diff --git a/examples/interactive_auth/openid.rs b/examples/interactive_auth/openid.rs index 68698a25..8a6abc2a 100644 --- a/examples/interactive_auth/openid.rs +++ b/examples/interactive_auth/openid.rs @@ -1,4 +1,5 @@ use anyhow::anyhow; +use graph_oauth::Secret; use graph_rs_sdk::{ http::Url, identity::interactive::WebViewAuthorizationEvent, @@ -34,7 +35,7 @@ async fn openid_authenticate( .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_redirect_uri(Url::parse(redirect_uri)?) - .with_interactive_auth(client_secret, Default::default()) + .with_interactive_auth(Secret(client_secret.to_string()), Default::default()) .into_credential_builder()?; debug!("{authorization_response:#?}"); @@ -59,7 +60,7 @@ async fn openid_authenticate2( .with_response_mode(ResponseMode::Fragment) .with_response_type(vec![ResponseType::Code, ResponseType::IdToken]) .with_redirect_uri(Url::parse(redirect_uri)?) - .with_interactive_auth(client_secret, Default::default())?; + .with_interactive_auth(Secret(client_secret.to_string()), Default::default())?; match auth_event { WebViewAuthorizationEvent::Authorized { diff --git a/graph-error/src/authorization_failure.rs b/graph-error/src/authorization_failure.rs index 94f92c15..e111a25f 100644 --- a/graph-error/src/authorization_failure.rs +++ b/graph-error/src/authorization_failure.rs @@ -1,5 +1,6 @@ -use crate::{ErrorMessage, IdentityResult}; +use crate::{ErrorMessage, IdentityResult, WebViewDeviceCodeError}; use tokio::sync::mpsc::error::SendTimeoutError; +use url::ParseError; pub type AF = AuthorizationFailure; @@ -127,6 +128,21 @@ impl From<serde_json::error::Error> for AuthExecutionError { } } +impl From<WebViewDeviceCodeError> for AuthExecutionError { + fn from(value: WebViewDeviceCodeError) -> Self { + AuthExecutionError::Authorization(AuthorizationFailure::msg_err( + "Unknown", + &value.to_string(), + )) + } +} + +impl From<url::ParseError> for AuthExecutionError { + fn from(value: ParseError) -> Self { + AuthExecutionError::Authorization(AuthorizationFailure::UrlParse(value)) + } +} + #[derive(Debug, thiserror::Error)] pub enum AuthTaskExecutionError<R> { #[error("{0:#?}")] diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 75dadfb5..2c59079b 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -531,8 +531,9 @@ impl DeviceCodePollingExecutor { } #[cfg(feature = "interactive-auth")] - pub fn with_interactive_authentication( + pub fn with_interactive_auth( &mut self, + options: WebViewOptions, ) -> AuthExecutionResult<(DeviceAuthorizationResponse, DeviceCodeInteractiveAuth)> { let response = self.credential.execute()?; let device_authorization_response: DeviceAuthorizationResponse = response.json()?; @@ -546,6 +547,7 @@ impl DeviceCodePollingExecutor { interval: Duration::from_secs(device_authorization_response.interval), verification_uri: device_authorization_response.verification_uri.clone(), verification_uri_complete: device_authorization_response.verification_uri_complete, + options, }, )) } @@ -581,27 +583,28 @@ pub struct DeviceCodeInteractiveAuth { interval: Duration, verification_uri: String, verification_uri_complete: Option<String>, + options: WebViewOptions, } #[allow(dead_code)] #[cfg(feature = "interactive-auth")] impl DeviceCodeInteractiveAuth { pub(crate) fn new( - mut credential: DeviceCodeCredential, + credential: DeviceCodeCredential, device_authorization_response: DeviceAuthorizationResponse, + options: WebViewOptions, ) -> DeviceCodeInteractiveAuth { - credential.with_device_code(device_authorization_response.device_code.clone()); DeviceCodeInteractiveAuth { credential, interval: Duration::from_secs(device_authorization_response.interval), verification_uri: device_authorization_response.verification_uri.clone(), verification_uri_complete: device_authorization_response.verification_uri_complete, + options, } } - pub fn interactive_webview_authentication( + pub fn poll( &mut self, - options: WebViewOptions, ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let url = { if let Some(url_complete) = self.verification_uri_complete.as_ref() { @@ -611,21 +614,22 @@ impl DeviceCodeInteractiveAuth { } }; - let (sender, _receiver) = std::sync::mpsc::channel(); + let (sender, receiver) = std::sync::mpsc::channel(); + let options = self.options.clone(); std::thread::spawn(move || { DeviceCodeCredential::run(url, vec![], options, sender).unwrap(); }); - self.poll() - } - - pub(crate) fn poll( - &mut self, - ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { let mut credential = self.credential.clone(); let mut interval = self.interval; + DeviceCodeInteractiveAuth::poll_internal(interval, credential) + } + pub(crate) fn poll_internal( + mut interval: Duration, + mut credential: DeviceCodeCredential, + ) -> Result<PublicClientApplication<DeviceCodeCredential>, WebViewDeviceCodeError> { loop { // Wait the amount of seconds that interval is. std::thread::sleep(interval); @@ -635,12 +639,17 @@ impl DeviceCodeInteractiveAuth { let status = http_response.status(); if status.is_success() { - let json = http_response.json().unwrap(); - let token: Token = serde_json::from_value(json) - .map_err(|err| Box::new(AuthExecutionError::from(err)))?; - let cache_id = credential.app_config.cache_id.clone(); - credential.token_cache.store(cache_id, token); - return Ok(PublicClientApplication::from(credential)); + return if let Some(json) = http_response.json() { + let token: Token = serde_json::from_value(json) + .map_err(|err| Box::new(AuthExecutionError::from(err)))?; + let cache_id = credential.app_config.cache_id.clone(); + credential.token_cache.store(cache_id, token); + Ok(PublicClientApplication::from(credential)) + } else { + Err(WebViewDeviceCodeError::DeviceCodePollingError( + http_response, + )) + }; } else { let json = http_response.json().unwrap(); let option_error = json["error"].as_str().map(|value| value.to_owned()); diff --git a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs index 6548e2e5..d561003e 100644 --- a/graph-oauth/src/identity/credentials/open_id_authorization_url.rs +++ b/graph-oauth/src/identity/credentials/open_id_authorization_url.rs @@ -25,6 +25,7 @@ use { HostOptions, InteractiveAuthEvent, UserEvents, WebViewAuth, WebViewAuthorizationEvent, WebViewHostValidator, WebViewOptions, }, + crate::Secret, graph_error::{WebViewError, WebViewResult}, tao::{event_loop::EventLoopProxy, window::Window}, wry::{WebView, WebViewBuilder}, @@ -562,11 +563,11 @@ impl OpenIdAuthorizationUrlParameterBuilder { #[cfg(feature = "interactive-auth")] pub fn with_interactive_auth( &self, - client_secret: impl AsRef<str>, + client_secret: Secret, options: WebViewOptions, ) -> WebViewResult<WebViewAuthorizationEvent<OpenIdCredentialBuilder>> { self.credential - .interactive_webview_authentication(client_secret, options) + .interactive_webview_authentication(client_secret.0, options) } pub fn build(&self) -> OpenIdAuthorizationUrlParameters { diff --git a/graph-oauth/src/interactive/interactive_auth.rs b/graph-oauth/src/interactive/interactive_auth.rs index 5136eaa4..0eb4a3e5 100644 --- a/graph-oauth/src/interactive/interactive_auth.rs +++ b/graph-oauth/src/interactive/interactive_auth.rs @@ -5,6 +5,7 @@ use std::sync::mpsc::Sender; use std::time::{Duration, Instant}; use tao::event::{Event, StartCause, WindowEvent}; use tao::event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy}; +use tao::platform::run_return::EventLoopExtRunReturn; use tao::window::{Window, WindowBuilder}; use url::Url; use wry::WebView; @@ -65,13 +66,13 @@ where options: WebViewOptions, sender: Sender<InteractiveAuthEvent>, ) -> anyhow::Result<()> { - let event_loop: EventLoop<UserEvents> = Self::event_loop(); + let mut event_loop: EventLoop<UserEvents> = Self::event_loop(); let proxy = event_loop.create_proxy(); let window = Self::window_builder(&options).build(&event_loop).unwrap(); let host_options = HostOptions::new(start_url, redirect_uris, options.ports.clone()); let webview = Self::webview(host_options, &window, proxy)?; - event_loop.run(move |event, _, control_flow| { + event_loop.run_return(move |event, _, control_flow| { if let Some(timeout) = options.timeout.as_ref() { *control_flow = ControlFlow::WaitUntil(*timeout); } else { @@ -163,6 +164,7 @@ where _ => (), } }); + Ok(()) } #[cfg(target_family = "windows")] From 34d470242babe42cab1052fbdeee79ce9d7719ea Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Sun, 5 May 2024 13:42:47 +0100 Subject: [PATCH 098/118] wip: try leaving the CalendarView in the BookingBusiness --- graph-codegen/src/settings/resource_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index fb96d3c0..13081c96 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -2703,7 +2703,7 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .build() .unwrap(), ResourceIdentity::BookingBusinesses => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) - .filter_path(vec!["appointments", "calendarView", "customQuestions", "customers", "services", "staffMembers"]) + .filter_path(vec!["appointments", "customQuestions", "customers", "services", "staffMembers"]) .trim_path_start("/solutions") .build() .unwrap(), From 8944a2dd8fdb5de050ffa7f4da25b81723ad8b99 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Sun, 5 May 2024 13:48:22 +0100 Subject: [PATCH 099/118] try: regen --- src/solutions/appointments/request.rs | 82 +++++----- src/solutions/booking_businesses/request.rs | 165 ++++++++++++-------- src/solutions/custom_questions/request.rs | 70 ++++----- src/solutions/customers/request.rs | 70 ++++----- src/solutions/mod.rs | 12 +- src/solutions/request.rs | 27 ++-- src/solutions/services/request.rs | 70 ++++----- src/solutions/staff_members/request.rs | 70 ++++----- 8 files changed, 291 insertions(+), 275 deletions(-) diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index 908040a7..ac44679d 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -2,52 +2,48 @@ use crate::api_default_imports::*; -resource_api_client!( - AppointmentsApiClient, - AppointmentsIdApiClient, - ResourceIdentity::Appointments -); +resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); impl AppointmentsApiClient { - post!( - doc: "Create bookingAppointment", - name: create_appointments, - path: "/appointments", - body: true - ); - get!( - doc: "List appointments", - name: list_appointments, - path: "/appointments" - ); - get!( - doc: "Get the number of the resource", - name: appointments, - path: "/appointments/$count" - ); + post!( + doc: "Create new navigation property to appointments for solutions", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "Get appointments from solutions", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: appointments, + path: "/appointments/$count" + ); } impl AppointmentsIdApiClient { - delete!( - doc: "Delete bookingAppointment", - name: delete_appointments, - path: "/appointments/{{RID}}" - ); - get!( - doc: "Get bookingAppointment", - name: get_appointments, - path: "/appointments/{{RID}}" - ); - patch!( - doc: "Update bookingAppointment", - name: update_appointments, - path: "/appointments/{{RID}}", - body: true - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/appointments/{{RID}}/cancel", - body: true - ); + delete!( + doc: "Delete navigation property appointments for solutions", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get appointments from solutions", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update the navigation property appointments in solutions", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); } diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index b400b041..b293de11 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -3,73 +3,110 @@ use crate::api_default_imports::*; use crate::solutions::*; -resource_api_client!( - BookingBusinessesApiClient, - BookingBusinessesIdApiClient, - ResourceIdentity::BookingBusinesses -); +resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); impl BookingBusinessesApiClient { - post!( - doc: "Create bookingBusiness", - name: create_booking_businesses, - path: "/bookingBusinesses", - body: true - ); - get!( - doc: "List bookingBusinesses", - name: list_booking_businesses, - path: "/bookingBusinesses" - ); - get!( - doc: "Get the number of the resource", - name: booking_businesses, - path: "/bookingBusinesses/$count" - ); + post!( + doc: "Create new navigation property to bookingBusinesses for solutions", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "Get bookingBusinesses from solutions", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: booking_businesses, + path: "/bookingBusinesses/$count" + ); } -impl BookingBusinessesIdApiClient { - api_client_link!(appointments, AppointmentsApiClient); - api_client_link_id!(service, ServicesIdApiClient); - api_client_link!(custom_questions, CustomQuestionsApiClient); - api_client_link!(services, ServicesApiClient); - api_client_link_id!(custom_question, CustomQuestionsIdApiClient); - api_client_link!(staff_members, StaffMembersApiClient); - api_client_link_id!(staff_member, StaffMembersIdApiClient); - api_client_link!(customers, CustomersApiClient); - api_client_link_id!(customer, CustomersIdApiClient); - api_client_link_id!(appointment, AppointmentsIdApiClient); +impl BookingBusinessesIdApiClient {api_client_link_id!(staff_member, StaffMembersIdApiClient); +api_client_link_id!(customer, CustomersIdApiClient); +api_client_link!(appointments, AppointmentsApiClient); +api_client_link!(services, ServicesApiClient); +api_client_link_id!(appointment, AppointmentsIdApiClient); +api_client_link_id!(service, ServicesIdApiClient); +api_client_link!(custom_questions, CustomQuestionsApiClient); +api_client_link!(customers, CustomersApiClient); +api_client_link_id!(custom_question, CustomQuestionsIdApiClient); +api_client_link!(staff_members, StaffMembersApiClient); - delete!( - doc: "Delete bookingBusiness", - name: delete_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - get!( - doc: "Get bookingBusiness", - name: get_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - patch!( - doc: "Update bookingbusiness", - name: update_booking_businesses, - path: "/bookingBusinesses/{{RID}}", - body: true - ); - post!( - doc: "Invoke action getStaffAvailability", - name: get_staff_availability, - path: "/bookingBusinesses/{{RID}}/getStaffAvailability", - body: true - ); - post!( - doc: "Invoke action publish", - name: publish, - path: "/bookingBusinesses/{{RID}}/publish" - ); - post!( - doc: "Invoke action unpublish", - name: unpublish, - path: "/bookingBusinesses/{{RID}}/unpublish" - ); + delete!( + doc: "Delete navigation property bookingBusinesses for solutions", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusinesses from solutions", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update the navigation property bookingBusinesses in solutions", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Create new navigation property to calendarView for solutions", + name: create_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView", + body: true + ); + get!( + doc: "Get calendarView from solutions", + name: list_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView" + ); + get!( + doc: "Get the number of the resource", + name: calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/$count" + ); + delete!( + doc: "Delete navigation property calendarView for solutions", + name: delete_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + params: booking_appointment_id + ); + get!( + doc: "Get calendarView from solutions", + name: get_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + params: booking_appointment_id + ); + patch!( + doc: "Update the navigation property calendarView in solutions", + name: update_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + body: true, + params: booking_appointment_id + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}/cancel", + body: true, + params: booking_appointment_id + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); } diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index 1a129677..1bf3fd49 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - CustomQuestionsApiClient, - CustomQuestionsIdApiClient, - ResourceIdentity::CustomQuestions -); +resource_api_client!(CustomQuestionsApiClient, CustomQuestionsIdApiClient, ResourceIdentity::CustomQuestions); impl CustomQuestionsApiClient { - post!( - doc: "Create bookingCustomQuestion", - name: create_custom_questions, - path: "/customQuestions", - body: true - ); - get!( - doc: "List customQuestions", - name: list_custom_questions, - path: "/customQuestions" - ); - get!( - doc: "Get the number of the resource", - name: custom_questions, - path: "/customQuestions/$count" - ); + post!( + doc: "Create new navigation property to customQuestions for solutions", + name: create_custom_questions, + path: "/customQuestions", + body: true + ); + get!( + doc: "Get customQuestions from solutions", + name: list_custom_questions, + path: "/customQuestions" + ); + get!( + doc: "Get the number of the resource", + name: custom_questions, + path: "/customQuestions/$count" + ); } impl CustomQuestionsIdApiClient { - delete!( - doc: "Delete bookingCustomQuestion", - name: delete_custom_questions, - path: "/customQuestions/{{RID}}" - ); - get!( - doc: "Get bookingCustomQuestion", - name: get_custom_questions, - path: "/customQuestions/{{RID}}" - ); - patch!( - doc: "Update bookingCustomQuestion", - name: update_custom_questions, - path: "/customQuestions/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property customQuestions for solutions", + name: delete_custom_questions, + path: "/customQuestions/{{RID}}" + ); + get!( + doc: "Get customQuestions from solutions", + name: get_custom_questions, + path: "/customQuestions/{{RID}}" + ); + patch!( + doc: "Update the navigation property customQuestions in solutions", + name: update_custom_questions, + path: "/customQuestions/{{RID}}", + body: true + ); } diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index 29a6019e..6725fcce 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - CustomersApiClient, - CustomersIdApiClient, - ResourceIdentity::Customers -); +resource_api_client!(CustomersApiClient, CustomersIdApiClient, ResourceIdentity::Customers); impl CustomersApiClient { - post!( - doc: "Create bookingCustomer", - name: create_customers, - path: "/customers", - body: true - ); - get!( - doc: "List customers", - name: list_customers, - path: "/customers" - ); - get!( - doc: "Get the number of the resource", - name: customers, - path: "/customers/$count" - ); + post!( + doc: "Create new navigation property to customers for solutions", + name: create_customers, + path: "/customers", + body: true + ); + get!( + doc: "Get customers from solutions", + name: list_customers, + path: "/customers" + ); + get!( + doc: "Get the number of the resource", + name: customers, + path: "/customers/$count" + ); } impl CustomersIdApiClient { - delete!( - doc: "Delete bookingCustomer", - name: delete_customers, - path: "/customers/{{RID}}" - ); - get!( - doc: "Get bookingCustomer", - name: get_customers, - path: "/customers/{{RID}}" - ); - patch!( - doc: "Update bookingCustomer", - name: update_customers, - path: "/customers/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property customers for solutions", + name: delete_customers, + path: "/customers/{{RID}}" + ); + get!( + doc: "Get customers from solutions", + name: get_customers, + path: "/customers/{{RID}}" + ); + patch!( + doc: "Update the navigation property customers in solutions", + name: update_customers, + path: "/customers/{{RID}}", + body: true + ); } diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index b4a42f77..d5f81a26 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,15 +1,15 @@ -mod appointments; +mod request; mod booking_businesses; +mod appointments; +mod services; mod custom_questions; mod customers; -mod request; -mod services; mod staff_members; -pub use appointments::*; +pub use request::*; pub use booking_businesses::*; +pub use appointments::*; +pub use services::*; pub use custom_questions::*; pub use customers::*; -pub use request::*; -pub use services::*; pub use staff_members::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index 5e4314c0..d8ef6951 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,19 +5,18 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient { - api_client_link!(booking_businesses, BookingBusinessesApiClient); - api_client_link_id!(booking_business, BookingBusinessesIdApiClient); +impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); +api_client_link!(booking_businesses, BookingBusinessesApiClient); - get!( - doc: "Get solutions", - name: get_solutions_root, - path: "/solutions" - ); - patch!( - doc: "Update solutions", - name: update_solutions_root, - path: "/solutions", - body: true - ); + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); } diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index c897619b..c869d30a 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - ServicesApiClient, - ServicesIdApiClient, - ResourceIdentity::Services -); +resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); impl ServicesApiClient { - post!( - doc: "Create bookingService", - name: create_services, - path: "/services", - body: true - ); - get!( - doc: "List services", - name: list_services, - path: "/services" - ); - get!( - doc: "Get the number of the resource", - name: services, - path: "/services/$count" - ); + post!( + doc: "Create new navigation property to services for solutions", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "Get services from solutions", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: services, + path: "/services/$count" + ); } impl ServicesIdApiClient { - delete!( - doc: "Delete bookingService", - name: delete_services, - path: "/services/{{RID}}" - ); - get!( - doc: "Get bookingService", - name: get_services, - path: "/services/{{RID}}" - ); - patch!( - doc: "Update bookingservice", - name: update_services, - path: "/services/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property services for solutions", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get services from solutions", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update the navigation property services in solutions", + name: update_services, + path: "/services/{{RID}}", + body: true + ); } diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index e3310d61..4279c4aa 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - StaffMembersApiClient, - StaffMembersIdApiClient, - ResourceIdentity::StaffMembers -); +resource_api_client!(StaffMembersApiClient, StaffMembersIdApiClient, ResourceIdentity::StaffMembers); impl StaffMembersApiClient { - post!( - doc: "Create bookingStaffMember", - name: create_staff_members, - path: "/staffMembers", - body: true - ); - get!( - doc: "List staffMembers", - name: list_staff_members, - path: "/staffMembers" - ); - get!( - doc: "Get the number of the resource", - name: staff_members, - path: "/staffMembers/$count" - ); + post!( + doc: "Create new navigation property to staffMembers for solutions", + name: create_staff_members, + path: "/staffMembers", + body: true + ); + get!( + doc: "Get staffMembers from solutions", + name: list_staff_members, + path: "/staffMembers" + ); + get!( + doc: "Get the number of the resource", + name: staff_members, + path: "/staffMembers/$count" + ); } impl StaffMembersIdApiClient { - delete!( - doc: "Delete bookingStaffMember", - name: delete_staff_members, - path: "/staffMembers/{{RID}}" - ); - get!( - doc: "Get bookingStaffMember", - name: get_staff_members, - path: "/staffMembers/{{RID}}" - ); - patch!( - doc: "Update bookingstaffmember", - name: update_staff_members, - path: "/staffMembers/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property staffMembers for solutions", + name: delete_staff_members, + path: "/staffMembers/{{RID}}" + ); + get!( + doc: "Get staffMembers from solutions", + name: get_staff_members, + path: "/staffMembers/{{RID}}" + ); + patch!( + doc: "Update the navigation property staffMembers in solutions", + name: update_staff_members, + path: "/staffMembers/{{RID}}", + body: true + ); } From 8404a1371f22cb111f3355bc8c6eb033fe1d1687 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Sun, 5 May 2024 17:43:57 +0100 Subject: [PATCH 100/118] docs: Add examples for solutions --- examples/solutions/business.rs | 59 ++++++ src/solutions/appointments/request.rs | 82 ++++---- src/solutions/booking_businesses/request.rs | 207 ++++++++++---------- src/solutions/custom_questions/request.rs | 70 +++---- src/solutions/customers/request.rs | 70 +++---- src/solutions/mod.rs | 12 +- src/solutions/request.rs | 27 +-- src/solutions/services/request.rs | 70 +++---- src/solutions/staff_members/request.rs | 70 +++---- 9 files changed, 376 insertions(+), 291 deletions(-) create mode 100644 examples/solutions/business.rs diff --git a/examples/solutions/business.rs b/examples/solutions/business.rs new file mode 100644 index 00000000..46199d74 --- /dev/null +++ b/examples/solutions/business.rs @@ -0,0 +1,59 @@ +use graph_rs_sdk::*; + +//businesses are the equivalent of booking pages +//creating a new business with the name "My Business" will create a booking page and once bookings +//are open, a new alias `mybusiness@tenant.com` will be created. If name already exists, the alias +//will be `mybusiness1@tenant.com`. +// +// +// + +async fn create_business(){ + let client = Graph::new("ACCESS_TOKEN"); + + + let data = serde_json::json!({ + "displayName": "My Business" + }); + + let body = Body::from(data.to_string()); + + let resp = client + .solutions() + .booking_businesses() + .create_booking_businesses(body) + .send() + .await + ; + + println!("{:#?}", resp); +} + +async fn get_businesses() { + let access_token = log_me_in().await.unwrap(); + let client = Graph::new(&access_token); + let bus = client.solutions().booking_businesses().list_booking_businesses().send().await.unwrap(); + + let businesses: serde_json::Value = bus.json().await.unwrap(); + println!("{:#}", businesses) +} + +async fn get_appointments() { + let access_token = log_me_in().await.unwrap(); + let client = Graph::new(&access_token); + + let appointments = client + .solutions() + //can be id retrieved from list_booking_businesses or pass the generated alias + .booking_business("mybusiness@tenant.com") + .appointments() + .list_appointments() + .send() + .await + .unwrap(); + + let app_json: serde_json::Value = appointments.json().await.unwrap(); + + println!("{:#?}", app_json); +} + diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index ac44679d..3b96ddc2 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -2,48 +2,52 @@ use crate::api_default_imports::*; -resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); +resource_api_client!( + AppointmentsApiClient, + AppointmentsIdApiClient, + ResourceIdentity::Appointments +); impl AppointmentsApiClient { - post!( - doc: "Create new navigation property to appointments for solutions", - name: create_appointments, - path: "/appointments", - body: true - ); - get!( - doc: "Get appointments from solutions", - name: list_appointments, - path: "/appointments" - ); - get!( - doc: "Get the number of the resource", - name: appointments, - path: "/appointments/$count" - ); + post!( + doc: "Create new navigation property to appointments for solutions", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "Get appointments from solutions", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: appointments, + path: "/appointments/$count" + ); } impl AppointmentsIdApiClient { - delete!( - doc: "Delete navigation property appointments for solutions", - name: delete_appointments, - path: "/appointments/{{RID}}" - ); - get!( - doc: "Get appointments from solutions", - name: get_appointments, - path: "/appointments/{{RID}}" - ); - patch!( - doc: "Update the navigation property appointments in solutions", - name: update_appointments, - path: "/appointments/{{RID}}", - body: true - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/appointments/{{RID}}/cancel", - body: true - ); + delete!( + doc: "Delete navigation property appointments for solutions", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get appointments from solutions", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update the navigation property appointments in solutions", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); } diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index b293de11..c2aa7b1c 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -3,110 +3,115 @@ use crate::api_default_imports::*; use crate::solutions::*; -resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); +resource_api_client!( + BookingBusinessesApiClient, + BookingBusinessesIdApiClient, + ResourceIdentity::BookingBusinesses +); impl BookingBusinessesApiClient { - post!( - doc: "Create new navigation property to bookingBusinesses for solutions", - name: create_booking_businesses, - path: "/bookingBusinesses", - body: true - ); - get!( - doc: "Get bookingBusinesses from solutions", - name: list_booking_businesses, - path: "/bookingBusinesses" - ); - get!( - doc: "Get the number of the resource", - name: booking_businesses, - path: "/bookingBusinesses/$count" - ); + post!( + doc: "Create new navigation property to bookingBusinesses for solutions", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "Get bookingBusinesses from solutions", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: booking_businesses, + path: "/bookingBusinesses/$count" + ); } -impl BookingBusinessesIdApiClient {api_client_link_id!(staff_member, StaffMembersIdApiClient); -api_client_link_id!(customer, CustomersIdApiClient); -api_client_link!(appointments, AppointmentsApiClient); -api_client_link!(services, ServicesApiClient); -api_client_link_id!(appointment, AppointmentsIdApiClient); -api_client_link_id!(service, ServicesIdApiClient); -api_client_link!(custom_questions, CustomQuestionsApiClient); -api_client_link!(customers, CustomersApiClient); -api_client_link_id!(custom_question, CustomQuestionsIdApiClient); -api_client_link!(staff_members, StaffMembersApiClient); +impl BookingBusinessesIdApiClient { + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link_id!(customer, CustomersIdApiClient); + api_client_link!(appointments, AppointmentsApiClient); + api_client_link!(services, ServicesApiClient); + api_client_link_id!(appointment, AppointmentsIdApiClient); + api_client_link_id!(service, ServicesIdApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); + api_client_link!(customers, CustomersApiClient); + api_client_link_id!(custom_question, CustomQuestionsIdApiClient); + api_client_link!(staff_members, StaffMembersApiClient); - delete!( - doc: "Delete navigation property bookingBusinesses for solutions", - name: delete_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - get!( - doc: "Get bookingBusinesses from solutions", - name: get_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - patch!( - doc: "Update the navigation property bookingBusinesses in solutions", - name: update_booking_businesses, - path: "/bookingBusinesses/{{RID}}", - body: true - ); - post!( - doc: "Create new navigation property to calendarView for solutions", - name: create_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView", - body: true - ); - get!( - doc: "Get calendarView from solutions", - name: list_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView" - ); - get!( - doc: "Get the number of the resource", - name: calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/$count" - ); - delete!( - doc: "Delete navigation property calendarView for solutions", - name: delete_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - params: booking_appointment_id - ); - get!( - doc: "Get calendarView from solutions", - name: get_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - params: booking_appointment_id - ); - patch!( - doc: "Update the navigation property calendarView in solutions", - name: update_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - body: true, - params: booking_appointment_id - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}/cancel", - body: true, - params: booking_appointment_id - ); - post!( - doc: "Invoke action getStaffAvailability", - name: get_staff_availability, - path: "/bookingBusinesses/{{RID}}/getStaffAvailability", - body: true - ); - post!( - doc: "Invoke action publish", - name: publish, - path: "/bookingBusinesses/{{RID}}/publish" - ); - post!( - doc: "Invoke action unpublish", - name: unpublish, - path: "/bookingBusinesses/{{RID}}/unpublish" - ); + delete!( + doc: "Delete navigation property bookingBusinesses for solutions", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusinesses from solutions", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update the navigation property bookingBusinesses in solutions", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Create new navigation property to calendarView for solutions", + name: create_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView", + body: true + ); + get!( + doc: "Get calendarView from solutions", + name: list_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView" + ); + get!( + doc: "Get the number of the resource", + name: calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/$count" + ); + delete!( + doc: "Delete navigation property calendarView for solutions", + name: delete_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + params: booking_appointment_id + ); + get!( + doc: "Get calendarView from solutions", + name: get_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + params: booking_appointment_id + ); + patch!( + doc: "Update the navigation property calendarView in solutions", + name: update_calendar_view, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", + body: true, + params: booking_appointment_id + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}/cancel", + body: true, + params: booking_appointment_id + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); } diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index 1bf3fd49..8eb3fcba 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(CustomQuestionsApiClient, CustomQuestionsIdApiClient, ResourceIdentity::CustomQuestions); +resource_api_client!( + CustomQuestionsApiClient, + CustomQuestionsIdApiClient, + ResourceIdentity::CustomQuestions +); impl CustomQuestionsApiClient { - post!( - doc: "Create new navigation property to customQuestions for solutions", - name: create_custom_questions, - path: "/customQuestions", - body: true - ); - get!( - doc: "Get customQuestions from solutions", - name: list_custom_questions, - path: "/customQuestions" - ); - get!( - doc: "Get the number of the resource", - name: custom_questions, - path: "/customQuestions/$count" - ); + post!( + doc: "Create new navigation property to customQuestions for solutions", + name: create_custom_questions, + path: "/customQuestions", + body: true + ); + get!( + doc: "Get customQuestions from solutions", + name: list_custom_questions, + path: "/customQuestions" + ); + get!( + doc: "Get the number of the resource", + name: custom_questions, + path: "/customQuestions/$count" + ); } impl CustomQuestionsIdApiClient { - delete!( - doc: "Delete navigation property customQuestions for solutions", - name: delete_custom_questions, - path: "/customQuestions/{{RID}}" - ); - get!( - doc: "Get customQuestions from solutions", - name: get_custom_questions, - path: "/customQuestions/{{RID}}" - ); - patch!( - doc: "Update the navigation property customQuestions in solutions", - name: update_custom_questions, - path: "/customQuestions/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property customQuestions for solutions", + name: delete_custom_questions, + path: "/customQuestions/{{RID}}" + ); + get!( + doc: "Get customQuestions from solutions", + name: get_custom_questions, + path: "/customQuestions/{{RID}}" + ); + patch!( + doc: "Update the navigation property customQuestions in solutions", + name: update_custom_questions, + path: "/customQuestions/{{RID}}", + body: true + ); } diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index 6725fcce..a6542fdd 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(CustomersApiClient, CustomersIdApiClient, ResourceIdentity::Customers); +resource_api_client!( + CustomersApiClient, + CustomersIdApiClient, + ResourceIdentity::Customers +); impl CustomersApiClient { - post!( - doc: "Create new navigation property to customers for solutions", - name: create_customers, - path: "/customers", - body: true - ); - get!( - doc: "Get customers from solutions", - name: list_customers, - path: "/customers" - ); - get!( - doc: "Get the number of the resource", - name: customers, - path: "/customers/$count" - ); + post!( + doc: "Create new navigation property to customers for solutions", + name: create_customers, + path: "/customers", + body: true + ); + get!( + doc: "Get customers from solutions", + name: list_customers, + path: "/customers" + ); + get!( + doc: "Get the number of the resource", + name: customers, + path: "/customers/$count" + ); } impl CustomersIdApiClient { - delete!( - doc: "Delete navigation property customers for solutions", - name: delete_customers, - path: "/customers/{{RID}}" - ); - get!( - doc: "Get customers from solutions", - name: get_customers, - path: "/customers/{{RID}}" - ); - patch!( - doc: "Update the navigation property customers in solutions", - name: update_customers, - path: "/customers/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property customers for solutions", + name: delete_customers, + path: "/customers/{{RID}}" + ); + get!( + doc: "Get customers from solutions", + name: get_customers, + path: "/customers/{{RID}}" + ); + patch!( + doc: "Update the navigation property customers in solutions", + name: update_customers, + path: "/customers/{{RID}}", + body: true + ); } diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index d5f81a26..b4a42f77 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,15 +1,15 @@ -mod request; -mod booking_businesses; mod appointments; -mod services; +mod booking_businesses; mod custom_questions; mod customers; +mod request; +mod services; mod staff_members; -pub use request::*; -pub use booking_businesses::*; pub use appointments::*; -pub use services::*; +pub use booking_businesses::*; pub use custom_questions::*; pub use customers::*; +pub use request::*; +pub use services::*; pub use staff_members::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index d8ef6951..8a3cfc46 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,18 +5,19 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); -api_client_link!(booking_businesses, BookingBusinessesApiClient); +impl SolutionsApiClient { + api_client_link_id!(booking_business, BookingBusinessesIdApiClient); + api_client_link!(booking_businesses, BookingBusinessesApiClient); - get!( - doc: "Get solutions", - name: get_solutions_root, - path: "/solutions" - ); - patch!( - doc: "Update solutions", - name: update_solutions_root, - path: "/solutions", - body: true - ); + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); } diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index c869d30a..17c76f99 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); +resource_api_client!( + ServicesApiClient, + ServicesIdApiClient, + ResourceIdentity::Services +); impl ServicesApiClient { - post!( - doc: "Create new navigation property to services for solutions", - name: create_services, - path: "/services", - body: true - ); - get!( - doc: "Get services from solutions", - name: list_services, - path: "/services" - ); - get!( - doc: "Get the number of the resource", - name: services, - path: "/services/$count" - ); + post!( + doc: "Create new navigation property to services for solutions", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "Get services from solutions", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: services, + path: "/services/$count" + ); } impl ServicesIdApiClient { - delete!( - doc: "Delete navigation property services for solutions", - name: delete_services, - path: "/services/{{RID}}" - ); - get!( - doc: "Get services from solutions", - name: get_services, - path: "/services/{{RID}}" - ); - patch!( - doc: "Update the navigation property services in solutions", - name: update_services, - path: "/services/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property services for solutions", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get services from solutions", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update the navigation property services in solutions", + name: update_services, + path: "/services/{{RID}}", + body: true + ); } diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index 4279c4aa..88e30067 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(StaffMembersApiClient, StaffMembersIdApiClient, ResourceIdentity::StaffMembers); +resource_api_client!( + StaffMembersApiClient, + StaffMembersIdApiClient, + ResourceIdentity::StaffMembers +); impl StaffMembersApiClient { - post!( - doc: "Create new navigation property to staffMembers for solutions", - name: create_staff_members, - path: "/staffMembers", - body: true - ); - get!( - doc: "Get staffMembers from solutions", - name: list_staff_members, - path: "/staffMembers" - ); - get!( - doc: "Get the number of the resource", - name: staff_members, - path: "/staffMembers/$count" - ); + post!( + doc: "Create new navigation property to staffMembers for solutions", + name: create_staff_members, + path: "/staffMembers", + body: true + ); + get!( + doc: "Get staffMembers from solutions", + name: list_staff_members, + path: "/staffMembers" + ); + get!( + doc: "Get the number of the resource", + name: staff_members, + path: "/staffMembers/$count" + ); } impl StaffMembersIdApiClient { - delete!( - doc: "Delete navigation property staffMembers for solutions", - name: delete_staff_members, - path: "/staffMembers/{{RID}}" - ); - get!( - doc: "Get staffMembers from solutions", - name: get_staff_members, - path: "/staffMembers/{{RID}}" - ); - patch!( - doc: "Update the navigation property staffMembers in solutions", - name: update_staff_members, - path: "/staffMembers/{{RID}}", - body: true - ); + delete!( + doc: "Delete navigation property staffMembers for solutions", + name: delete_staff_members, + path: "/staffMembers/{{RID}}" + ); + get!( + doc: "Get staffMembers from solutions", + name: get_staff_members, + path: "/staffMembers/{{RID}}" + ); + patch!( + doc: "Update the navigation property staffMembers in solutions", + name: update_staff_members, + path: "/staffMembers/{{RID}}", + body: true + ); } From 8f8609627e2f27cb9d1d2a705b7e73e3a378fcde Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 23 May 2024 01:38:50 -0400 Subject: [PATCH 101/118] Update documentation for 2.0.0 release --- README.md | 46 +++++++++--------- .../authorization_sign_in/openid_connect.rs | 4 +- examples/identity_platform_auth/README.md | 47 ++++++++++++++++++- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3683a49c..cf838bc3 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,16 @@ ### Rust SDK Client for Microsoft Graph and Microsoft Identity Platform -### Available on [crates.io](https://crates.io/crates/graph-rs-sdk/1.1.4) - v2.0.0 - Latest Stable Version +### Available on [crates.io](https://crates.io/crates/graph-rs-sdk/2.0.0) - v2.0.0 - Latest Stable Version #### Features: -Microsoft Graph V1 and Beta API Client +[Microsoft Graph V1 and Beta API Client](#graph-client) - Wide support for Graph APIs - Paging using Streaming, Channels, or Iterators - Upload Sessions, OData Queries, and File Downloads -Microsoft Identity Platform (Getting Access Tokens) +[Microsoft Identity Platform (Getting Access Tokens)](#oauth-and-openid) - Auth Code, Client Credentials, Device Code, OpenId - In Memory Token Cache - Automatic Token Refresh @@ -60,9 +60,9 @@ is enabled so feel free to stop by there with any questions or feature requests an issue first. Features can be requested through issues or discussions. Either way works. Other than that feel free to ask questions, provide tips to others, and talk about the project in general. -## Table Of Contents +## Features -Graph Client +### Graph Client * [Usage](#usage) * [Async and Blocking Client](#async-and-blocking-client) @@ -78,7 +78,7 @@ Graph Client * [Wiki](#wiki) * [Feature Requests for Bug Reports](#feature-requests-or-bug-reports) -OAuth and Openid +### OAuth and Openid * [OAuth - Getting Access Tokens](#oauth---getting-access-tokens) * [Identity Platform Support](#identity-platform-support) @@ -92,16 +92,16 @@ OAuth and Openid * [Client Secret Environment Credential](#client-secret-environment-credential) * [Resource Owner Password Credential](#resource-owner-password-credential) * [Automatic Token Refresh](#automatic-token-refresh) - * [Interactive Authentication](#interactive-authentication) + * [Interactive Authentication (WebView)](#interactive-authentication) -[Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) -- [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) -- [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) -- [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/client_credentials)) -- [Url Builders For Flows Using Sign In To Get Authorization Code - Build Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/authorization_sign_in) -- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/identity_platform_auth)) -- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0/examples/certificate_auth) +[Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth) +- [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/auth_code_grant) +- [OpenId](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/openid) +- [Client Credentials](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/client_credentials) +- [Url Builders For Flows Using Sign In To Get Authorization Code - Build Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/authorization_sign_in) +- [Interactive Auth Examples (feature = `interactive-auth`)](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth) +- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/certificate_auth) ### What APIs are available @@ -1027,13 +1027,13 @@ Support for: #### Detailed Examples: -- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth) - - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/auth_code_grant) - - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/openid)) - - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth/client_credentials)) -- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/authorization_sign_in) -- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/identity_platform_auth)) -- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/v2.0.0-beta.0/examples/certificate_auth) +- [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth) + - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/auth_code_grant) + - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/openid)) + - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/client_credentials)) +- [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/authorization_sign_in) +- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth)) +- [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/certificate_auth) There are two main types for building your chosen OAuth or OpenId Connect Flow. @@ -1277,7 +1277,9 @@ async fn build_client( Requires Feature `interactive-auth` -NOTE: Device code interactive auth does not currently work in async code. +**NOTE:** Running interactive-auth in an asynchronous context may lead to crashes. +Additionally, Device code interactive auth does not currently work in async code. +We are working to address these issues in a release after version 2.0.0 ```toml [dependencies] diff --git a/examples/authorization_sign_in/openid_connect.rs b/examples/authorization_sign_in/openid_connect.rs index d0827d8e..11c6f503 100644 --- a/examples/authorization_sign_in/openid_connect.rs +++ b/examples/authorization_sign_in/openid_connect.rs @@ -13,7 +13,7 @@ use url::Url; // If you are listening on a server use the response mod ResponseMode::FormPost. // Servers do not get sent the URL query and so in order to get what would normally be in // the query of URL use a form post which sends the data as a POST http request. -// Furthermore openid does not support the query response mode but does support fragment. +// Furthermore, openid does not support the query response mode but does support fragment. // The URL builder below will create the full URL with the query that you will // need to send the user to such as redirecting the page they are on when using @@ -23,7 +23,7 @@ use url::Url; // Use the form post response mode when listening on a server instead // of the URL query because the the query does not get sent to servers. -fn openid_authorization_url3( +fn openid_authorization_url( client_id: &str, tenant: &str, redirect_uri: &str, diff --git a/examples/identity_platform_auth/README.md b/examples/identity_platform_auth/README.md index b0d53f6a..0c50e950 100644 --- a/examples/identity_platform_auth/README.md +++ b/examples/identity_platform_auth/README.md @@ -1,15 +1,19 @@ # Identity Overview +The following provides a brief overview of the credential types. For more comprehensive examples +see the individual code examples in this directory. + There are two main types for building your chosen OAuth or OpenId Connect Flow. - `PublicClientApplication` - `ConfidentialClientApplication` -## Table Of Contents +## Overview Of Credential Types * [Credentials](#credentials) * [Authorization Code Grant](#authorization-code-grant) + * [OpenId](#openid) * [Client Credentials](#client-credentials) * [Client Secret Credential](#client-secret-credential) * [Environment Credentials](#environment-credentials) @@ -24,6 +28,9 @@ The authorization code grant is considered a confidential client (except in the and we can get an access token by using the authorization code returned in the query of the URL on redirect after sign in is performed by the user. +For more information see [Authorization Code Grant](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) +documentation from Microsoft. + ```rust use graph_rs_sdk::{ Graph, @@ -50,6 +57,41 @@ async fn build_client( } ``` +### OpenId + +OpenID Connect (OIDC) extends the OAuth 2.0 authorization protocol for use as an additional authentication protocol. +You can use OIDC to enable single sign-on (SSO) between your OAuth-enabled applications by using a security token +called an ID token. + +For more information see [Open ID Connect](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc) +documentation from Microsoft. + +```rust +use graph_rs_sdk::{ + Graph, + oauth::ConfidentialClientApplication, +}; + +fn build_client( + tenant_id: &str, + client_id: &str, + client_secret: &str, + redirect_uri: Url, + scope: Vec<&str>, + id_token: IdToken, +) -> GraphClient { + let mut confidential_client = ConfidentialClientApplication::builder(client_id) + .with_openid(id_token.code.unwrap(), client_secret) + .with_tenant(tenant_id) + .with_redirect_uri(redirect_uri) + .with_scope(scope) + .build(); + + GraphClient::from(&confidential_client) +} + +``` + ## Client Credentials The OAuth 2.0 client credentials grant flow permits a web service (confidential client) to use its own credentials, @@ -62,6 +104,9 @@ Client credentials flow requires a one time administrator acceptance of the permissions for your apps scopes. To see an example of building the URL to sign in and accept permissions as an administrator see [Admin Consent Example](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth/client_credentials/client_credentials_admin_consent.rs) +For more information see [Client Credentials](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) +documentation from Microsoft. + ### Client Secret Credential ```rust From 79a029d056657996aeb0b2b6c6e3365c9b3fe1f9 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 23 May 2024 01:56:23 -0400 Subject: [PATCH 102/118] Update version for 2.0.0 release --- Cargo.toml | 10 +++++----- README.md | 9 ++++++--- examples/interactive_auth/INTERACTIVE_AUTH.md | 5 +++++ graph-core/Cargo.toml | 2 +- graph-error/Cargo.toml | 2 +- graph-http/Cargo.toml | 2 +- graph-oauth/Cargo.toml | 2 +- .../src/identity/credentials/device_code_credential.rs | 6 +++--- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2679f696..097564b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-rs-sdk" -version = "2.0.0-beta.0" +version = "2.0.0" authors = ["sreeise"] edition = "2021" readme = "README.md" @@ -37,10 +37,10 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" -graph-oauth = { path = "./graph-oauth", version = "2.0.0-beta.0", default-features=false } -graph-http = { path = "./graph-http", version = "2.0.0-beta.0", default-features=false } -graph-error = { path = "./graph-error", version = "0.3.0-beta.0" } -graph-core = { path = "./graph-core", version = "2.0.0-beta.0", default-features=false } +graph-oauth = { path = "./graph-oauth", version = "2.0.0", default-features=false } +graph-http = { path = "./graph-http", version = "2.0.0", default-features=false } +graph-error = { path = "./graph-error", version = "0.3.0" } +graph-core = { path = "./graph-core", version = "2.0.0", default-features=false } # When updating or adding new features to this or dependent crates run # cargo tree -e features -i graph-rs-sdk diff --git a/README.md b/README.md index cf838bc3..8b7ca22d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ - Interactive WebView Auth (feature = `interactive-auth`) - X509 Certificate (feature = `openssl`) and Proof Key Code Exchange (PKCE) Support +Currently only an in-memory token cache is available for token persistence. +The following persistence mechanisms are being actively developed and will be in a post-2.0.0 release ```toml graph-rs-sdk = "2.0.0" @@ -1277,9 +1279,10 @@ async fn build_client( Requires Feature `interactive-auth` -**NOTE:** Running interactive-auth in an asynchronous context may lead to crashes. -Additionally, Device code interactive auth does not currently work in async code. -We are working to address these issues in a release after version 2.0.0 +**WARNING:** Running interactive-auth in an asynchronous context may lead to crashes in some scenarios. +We recommend thoroughly testing in order to ensure you are able to use interactive-auth for your use case. +Additionally, Device code interactive auth does not currently work in async code. +We are working to address these issues in a post 2.0.0 release. ```toml [dependencies] diff --git a/examples/interactive_auth/INTERACTIVE_AUTH.md b/examples/interactive_auth/INTERACTIVE_AUTH.md index a72d15bf..f6ff2e84 100644 --- a/examples/interactive_auth/INTERACTIVE_AUTH.md +++ b/examples/interactive_auth/INTERACTIVE_AUTH.md @@ -1,5 +1,10 @@ # Interactive Authentication +**WARNING:** Running interactive-auth in an asynchronous context may lead to crashes in some scenarios. +We recommend thoroughly testing in order to ensure you are able to use interactive-auth for your use case. +Additionally, Device code interactive auth does not currently work in async code. +We are working to address these issues in a post 2.0.0 release. + Interactive Authentication uses a webview to perform sign in and handle the redirect uri making it easy for you to integrate the sdk into your application. diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index e3a4ce21..61b2370f 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-core" -version = "2.0.0-beta.0" +version = "2.0.0" authors = ["sreeise"] edition = "2021" license = "MIT" diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 7728b45f..55dbefa2 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-error" -version = "0.3.0-beta.0" +version = "0.3.0" authors = ["sreeise"] edition = "2021" license = "MIT" diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index 7fe45086..b9c1a23b 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-http" -version = "2.0.0-beta.0" +version = "2.0.0" authors = ["sreeise"] edition = "2021" license = "MIT" diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index e0c5aaa1..16627a20 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graph-oauth" -version = "2.0.0-beta.0" +version = "2.0.0" authors = ["sreeise"] edition = "2021" license = "MIT" diff --git a/graph-oauth/src/identity/credentials/device_code_credential.rs b/graph-oauth/src/identity/credentials/device_code_credential.rs index 2c59079b..c7fa7bbe 100644 --- a/graph-oauth/src/identity/credentials/device_code_credential.rs +++ b/graph-oauth/src/identity/credentials/device_code_credential.rs @@ -614,15 +614,15 @@ impl DeviceCodeInteractiveAuth { } }; - let (sender, receiver) = std::sync::mpsc::channel(); + let (sender, _receiver) = std::sync::mpsc::channel(); let options = self.options.clone(); std::thread::spawn(move || { DeviceCodeCredential::run(url, vec![], options, sender).unwrap(); }); - let mut credential = self.credential.clone(); - let mut interval = self.interval; + let credential = self.credential.clone(); + let interval = self.interval; DeviceCodeInteractiveAuth::poll_internal(interval, credential) } From 3cdfaebfcc039c431f5876da6b65a10958ec64d6 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 23 May 2024 02:45:33 -0400 Subject: [PATCH 103/118] Fix graph-error dependency version in graph-core - issue is only in master branch on GitHub and and is not an issue in 2.0.0 release --- graph-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index 61b2370f..1f3cc129 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -27,7 +27,7 @@ remain = "0.2.6" tracing = "0.1.37" url = { version = "2", features = ["serde"] } -graph-error = { version = "0.3.0-beta.0", path = "../graph-error" } +graph-error = { version = "0.3.0", path = "../graph-error" } [features] default = ["native-tls"] From b35f98e641ed17ad13f21c5729d1ffd8894394cc Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 23 May 2024 03:06:30 -0400 Subject: [PATCH 104/118] Update README.md --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8b7ca22d..e5b936bd 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,6 @@ - Interactive WebView Auth (feature = `interactive-auth`) - X509 Certificate (feature = `openssl`) and Proof Key Code Exchange (PKCE) Support -Currently only an in-memory token cache is available for token persistence. -The following persistence mechanisms are being actively developed and will be in a post-2.0.0 release - ```toml graph-rs-sdk = "2.0.0" tokio = { version = "1.25.0", features = ["full"] } @@ -94,6 +91,8 @@ Other than that feel free to ask questions, provide tips to others, and talk abo * [Client Secret Environment Credential](#client-secret-environment-credential) * [Resource Owner Password Credential](#resource-owner-password-credential) * [Automatic Token Refresh](#automatic-token-refresh) + * Currently only an in-memory token cache is available for token persistence. Development for other persistence mechanisms such as Azure Key Vault and Desktop mechanisms, such as MacOS KeyChain, are being actively developed and will be in a post-2.0.0 release. + You can track this progress in https://github.com/sreeise/graph-rs-sdk/issues/432 * [Interactive Authentication (WebView)](#interactive-authentication) @@ -1223,7 +1222,7 @@ pub fn username_password() -> anyhow::Result<GraphClient> { ### Automatic Token Refresh -The client stores tokens using an in memory cache. +The client stores tokens using an in memory cache. For other persistence mechanisms see [Token Persistence Mechanism Development](#token-persistence-mechanism-development) Using automatic token refresh requires getting a refresh token as part of the token response. To get a refresh token you must include the `offline_access` scope. @@ -1274,6 +1273,12 @@ async fn build_client( } ``` +#### Token Persistence Mechanism Development + +Currently only an in-memory token cache is available for token persistence. +Development for other persistence mechanisms such as Azure Key Vault and Desktop mechanisms, such as MacOS KeyChain, are being actively developed and will be in a post-2.0.0 release. +You can track this progress in https://github.com/sreeise/graph-rs-sdk/issues/432 + ### Interactive Authentication From ea02384be2ab592dc72c2c0fc38d9c11fc878a40 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Mon, 27 May 2024 13:16:12 +0100 Subject: [PATCH 105/118] fix: change method names for resources to maintain naming conventions for getting $count - get_[resource]_count - appointments - custom_questions - customers - services - booking_business - staff_members - fix solutions example --- .../mail_folders_and_messages/messages.rs | 2 +- examples/solutions/business.rs | 3 +- .../src/settings/method_macro_modifier.rs | 34 +++++++++++++++++-- src/solutions/appointments/request.rs | 12 +++---- src/solutions/booking_businesses/request.rs | 24 ++++++------- src/solutions/custom_questions/request.rs | 12 +++---- src/solutions/customers/request.rs | 12 +++---- src/solutions/services/request.rs | 12 +++---- src/solutions/staff_members/request.rs | 10 +++--- 9 files changed, 75 insertions(+), 46 deletions(-) diff --git a/examples/mail_folders_and_messages/messages.rs b/examples/mail_folders_and_messages/messages.rs index 5d156237..7270550d 100644 --- a/examples/mail_folders_and_messages/messages.rs +++ b/examples/mail_folders_and_messages/messages.rs @@ -1,5 +1,5 @@ -use graph_rs_sdk::*; use graph_rs_sdk::header::{HeaderValue, CONTENT_LENGTH}; +use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; diff --git a/examples/solutions/business.rs b/examples/solutions/business.rs index 46199d74..bdcb31c3 100644 --- a/examples/solutions/business.rs +++ b/examples/solutions/business.rs @@ -23,8 +23,7 @@ async fn create_business(){ .booking_businesses() .create_booking_businesses(body) .send() - .await - ; + .await; println!("{:#?}", resp); } diff --git a/graph-codegen/src/settings/method_macro_modifier.rs b/graph-codegen/src/settings/method_macro_modifier.rs index 4470b7f0..3a3e47e6 100644 --- a/graph-codegen/src/settings/method_macro_modifier.rs +++ b/graph-codegen/src/settings/method_macro_modifier.rs @@ -1067,6 +1067,36 @@ pub fn get_method_macro_modifiers(resource_identity: ResourceIdentity) -> Vec<Me GeneratedMacroType::FnName("bin_20_ct") ), ], - _ => vec![], - } + ResourceIdentity::Appointments => vec![ + MethodMacroModifier::fn_name_and_path( + "appointments", "/appointments/$count", + GeneratedMacroType::FnName("get_appointments_count") + )], + ResourceIdentity::CustomQuestions => vec![ + MethodMacroModifier::fn_name_and_path( + "custom_questions", "/customQuestions/$count", + GeneratedMacroType::FnName("get_custom_questions_count") + )], + ResourceIdentity::Customers => vec![ + MethodMacroModifier::fn_name_and_path( + "customers", "/customers/$count", + GeneratedMacroType::FnName("get_customers_count") + )], + ResourceIdentity::Services => vec![ + MethodMacroModifier::fn_name_and_path( + "services", "/services/$count", + GeneratedMacroType::FnName("get_services_count") + )], + ResourceIdentity::BookingBusinesses => vec![ + MethodMacroModifier::fn_name_and_path( + "booking_businesses", "/bookingBusinesses/$count", + GeneratedMacroType::FnName("get_booking_businesses_count") + )], + ResourceIdentity::StaffMembers => vec![ + MethodMacroModifier::fn_name_and_path( + "staff_members", "/staff_members/$count", + GeneratedMacroType::FnName("get_staff_members_count") + )], + _ => vec![], + } } diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index 3b96ddc2..e4c12853 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -10,36 +10,36 @@ resource_api_client!( impl AppointmentsApiClient { post!( - doc: "Create new navigation property to appointments for solutions", + doc: "Create bookingAppointment", name: create_appointments, path: "/appointments", body: true ); get!( - doc: "Get appointments from solutions", + doc: "List appointments", name: list_appointments, path: "/appointments" ); get!( doc: "Get the number of the resource", - name: appointments, + name: get_appointments_count, path: "/appointments/$count" ); } impl AppointmentsIdApiClient { delete!( - doc: "Delete navigation property appointments for solutions", + doc: "Delete bookingAppointment", name: delete_appointments, path: "/appointments/{{RID}}" ); get!( - doc: "Get appointments from solutions", + doc: "Get bookingAppointment", name: get_appointments, path: "/appointments/{{RID}}" ); patch!( - doc: "Update the navigation property appointments in solutions", + doc: "Update bookingAppointment", name: update_appointments, path: "/appointments/{{RID}}", body: true diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index c2aa7b1c..a4c641a0 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -11,47 +11,47 @@ resource_api_client!( impl BookingBusinessesApiClient { post!( - doc: "Create new navigation property to bookingBusinesses for solutions", + doc: "Create bookingBusiness", name: create_booking_businesses, path: "/bookingBusinesses", body: true ); get!( - doc: "Get bookingBusinesses from solutions", + doc: "List bookingBusinesses", name: list_booking_businesses, path: "/bookingBusinesses" ); get!( doc: "Get the number of the resource", - name: booking_businesses, + name: get_booking_businesses_count, path: "/bookingBusinesses/$count" ); } impl BookingBusinessesIdApiClient { - api_client_link_id!(staff_member, StaffMembersIdApiClient); api_client_link_id!(customer, CustomersIdApiClient); - api_client_link!(appointments, AppointmentsApiClient); api_client_link!(services, ServicesApiClient); - api_client_link_id!(appointment, AppointmentsIdApiClient); api_client_link_id!(service, ServicesIdApiClient); - api_client_link!(custom_questions, CustomQuestionsApiClient); - api_client_link!(customers, CustomersApiClient); + api_client_link!(appointments, AppointmentsApiClient); api_client_link_id!(custom_question, CustomQuestionsIdApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); api_client_link!(staff_members, StaffMembersApiClient); + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link!(customers, CustomersApiClient); + api_client_link_id!(appointment, AppointmentsIdApiClient); delete!( - doc: "Delete navigation property bookingBusinesses for solutions", + doc: "Delete bookingBusiness", name: delete_booking_businesses, path: "/bookingBusinesses/{{RID}}" ); get!( - doc: "Get bookingBusinesses from solutions", + doc: "Get bookingBusiness", name: get_booking_businesses, path: "/bookingBusinesses/{{RID}}" ); patch!( - doc: "Update the navigation property bookingBusinesses in solutions", + doc: "Update bookingbusiness", name: update_booking_businesses, path: "/bookingBusinesses/{{RID}}", body: true @@ -63,7 +63,7 @@ impl BookingBusinessesIdApiClient { body: true ); get!( - doc: "Get calendarView from solutions", + doc: "List business calendarView", name: list_calendar_view, path: "/bookingBusinesses/{{RID}}/calendarView" ); diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index 8eb3fcba..fdadb442 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -10,36 +10,36 @@ resource_api_client!( impl CustomQuestionsApiClient { post!( - doc: "Create new navigation property to customQuestions for solutions", + doc: "Create bookingCustomQuestion", name: create_custom_questions, path: "/customQuestions", body: true ); get!( - doc: "Get customQuestions from solutions", + doc: "List customQuestions", name: list_custom_questions, path: "/customQuestions" ); get!( doc: "Get the number of the resource", - name: custom_questions, + name: get_custom_questions_count, path: "/customQuestions/$count" ); } impl CustomQuestionsIdApiClient { delete!( - doc: "Delete navigation property customQuestions for solutions", + doc: "Delete bookingCustomQuestion", name: delete_custom_questions, path: "/customQuestions/{{RID}}" ); get!( - doc: "Get customQuestions from solutions", + doc: "Get bookingCustomQuestion", name: get_custom_questions, path: "/customQuestions/{{RID}}" ); patch!( - doc: "Update the navigation property customQuestions in solutions", + doc: "Update bookingCustomQuestion", name: update_custom_questions, path: "/customQuestions/{{RID}}", body: true diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index a6542fdd..3e798905 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -10,36 +10,36 @@ resource_api_client!( impl CustomersApiClient { post!( - doc: "Create new navigation property to customers for solutions", + doc: "Create bookingCustomer", name: create_customers, path: "/customers", body: true ); get!( - doc: "Get customers from solutions", + doc: "List customers", name: list_customers, path: "/customers" ); get!( doc: "Get the number of the resource", - name: customers, + name: get_customers_count, path: "/customers/$count" ); } impl CustomersIdApiClient { delete!( - doc: "Delete navigation property customers for solutions", + doc: "Delete bookingCustomer", name: delete_customers, path: "/customers/{{RID}}" ); get!( - doc: "Get customers from solutions", + doc: "Get bookingCustomer", name: get_customers, path: "/customers/{{RID}}" ); patch!( - doc: "Update the navigation property customers in solutions", + doc: "Update bookingCustomer", name: update_customers, path: "/customers/{{RID}}", body: true diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index 17c76f99..db496cd9 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -10,36 +10,36 @@ resource_api_client!( impl ServicesApiClient { post!( - doc: "Create new navigation property to services for solutions", + doc: "Create bookingService", name: create_services, path: "/services", body: true ); get!( - doc: "Get services from solutions", + doc: "List services", name: list_services, path: "/services" ); get!( doc: "Get the number of the resource", - name: services, + name: get_services_count, path: "/services/$count" ); } impl ServicesIdApiClient { delete!( - doc: "Delete navigation property services for solutions", + doc: "Delete bookingService", name: delete_services, path: "/services/{{RID}}" ); get!( - doc: "Get services from solutions", + doc: "Get bookingService", name: get_services, path: "/services/{{RID}}" ); patch!( - doc: "Update the navigation property services in solutions", + doc: "Update bookingservice", name: update_services, path: "/services/{{RID}}", body: true diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index 88e30067..e3310d61 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -10,13 +10,13 @@ resource_api_client!( impl StaffMembersApiClient { post!( - doc: "Create new navigation property to staffMembers for solutions", + doc: "Create bookingStaffMember", name: create_staff_members, path: "/staffMembers", body: true ); get!( - doc: "Get staffMembers from solutions", + doc: "List staffMembers", name: list_staff_members, path: "/staffMembers" ); @@ -29,17 +29,17 @@ impl StaffMembersApiClient { impl StaffMembersIdApiClient { delete!( - doc: "Delete navigation property staffMembers for solutions", + doc: "Delete bookingStaffMember", name: delete_staff_members, path: "/staffMembers/{{RID}}" ); get!( - doc: "Get staffMembers from solutions", + doc: "Get bookingStaffMember", name: get_staff_members, path: "/staffMembers/{{RID}}" ); patch!( - doc: "Update the navigation property staffMembers in solutions", + doc: "Update bookingstaffmember", name: update_staff_members, path: "/staffMembers/{{RID}}", body: true From 2b9df2482b7689b8203f0dbae4a42af207fed9b1 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Tue, 28 May 2024 09:25:44 +0100 Subject: [PATCH 106/118] try: remove calendarView and try linkup? --- .../src/settings/resource_settings.rs | 11 +- src/client/graph.rs | 4 +- src/lib.rs | 2 +- src/solutions/appointments/request.rs | 82 +++++---- src/solutions/booking_businesses/request.rs | 165 +++++++----------- src/solutions/custom_questions/request.rs | 70 ++++---- src/solutions/customers/request.rs | 70 ++++---- src/solutions/mod.rs | 12 +- src/solutions/request.rs | 27 ++- src/solutions/services/request.rs | 70 ++++---- src/solutions/staff_members/request.rs | 70 ++++---- 11 files changed, 262 insertions(+), 321 deletions(-) diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 13081c96..3174d0a5 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -2703,8 +2703,17 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .build() .unwrap(), ResourceIdentity::BookingBusinesses => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) - .filter_path(vec!["appointments", "customQuestions", "customers", "services", "staffMembers"]) + // look at ~ 2506 .trim_path_start("/solutions") + .filter_path(vec!["appointments", "calendarView", "customQuestions", "customers", "services", "staffMembers"]) + .imports(vec!["crate::users::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("BookingBusinessesIdApiClient"), vec![ + //ApiClientLinkSettings(None, vec![ + ApiClientLink::Struct("calendar_views", "CalendarViewApiClient"), + ApiClientLink::StructId("calendar_view", "CalendarViewIdApiClient"), + ]) + ]) .build() .unwrap(), ResourceIdentity::Appointments => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) diff --git a/src/client/graph.rs b/src/client/graph.rs index 2e409d14..ba84a7f1 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -56,7 +56,7 @@ use crate::reports::ReportsApiClient; use crate::schema_extensions::{SchemaExtensionsApiClient, SchemaExtensionsIdApiClient}; use crate::service_principals::{ServicePrincipalsApiClient, ServicePrincipalsIdApiClient}; use crate::sites::{SitesApiClient, SitesIdApiClient}; -use crate::solutions::SolutionsApiClient; +//use crate::solutions::SolutionsApiClient; use crate::subscribed_skus::SubscribedSkusApiClient; use crate::subscriptions::{SubscriptionsApiClient, SubscriptionsIdApiClient}; use crate::teams::{TeamsApiClient, TeamsIdApiClient}; @@ -379,7 +379,7 @@ impl Graph { api_client_impl!(sites, SitesApiClient, site, SitesIdApiClient); - api_client_impl!(solutions, SolutionsApiClient); + //api_client_impl!(solutions, SolutionsApiClient); api_client_impl!( subscribed_skus, diff --git a/src/lib.rs b/src/lib.rs index e02d1ea3..9fcb42d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ pub mod reports; pub mod schema_extensions; pub mod service_principals; pub mod sites; -pub mod solutions; +//pub mod solutions; pub mod subscribed_skus; pub mod subscriptions; pub mod teams; diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index e4c12853..05ba48d8 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -2,52 +2,48 @@ use crate::api_default_imports::*; -resource_api_client!( - AppointmentsApiClient, - AppointmentsIdApiClient, - ResourceIdentity::Appointments -); +resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); impl AppointmentsApiClient { - post!( - doc: "Create bookingAppointment", - name: create_appointments, - path: "/appointments", - body: true - ); - get!( - doc: "List appointments", - name: list_appointments, - path: "/appointments" - ); - get!( - doc: "Get the number of the resource", - name: get_appointments_count, - path: "/appointments/$count" - ); + post!( + doc: "Create bookingAppointment", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "List appointments", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: get_appointments_count, + path: "/appointments/$count" + ); } impl AppointmentsIdApiClient { - delete!( - doc: "Delete bookingAppointment", - name: delete_appointments, - path: "/appointments/{{RID}}" - ); - get!( - doc: "Get bookingAppointment", - name: get_appointments, - path: "/appointments/{{RID}}" - ); - patch!( - doc: "Update bookingAppointment", - name: update_appointments, - path: "/appointments/{{RID}}", - body: true - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/appointments/{{RID}}/cancel", - body: true - ); + delete!( + doc: "Delete bookingAppointment", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get bookingAppointment", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update bookingAppointment", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); } diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index a4c641a0..019262b4 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -3,115 +3,68 @@ use crate::api_default_imports::*; use crate::solutions::*; -resource_api_client!( - BookingBusinessesApiClient, - BookingBusinessesIdApiClient, - ResourceIdentity::BookingBusinesses -); +resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); impl BookingBusinessesApiClient { - post!( - doc: "Create bookingBusiness", - name: create_booking_businesses, - path: "/bookingBusinesses", - body: true - ); - get!( - doc: "List bookingBusinesses", - name: list_booking_businesses, - path: "/bookingBusinesses" - ); - get!( - doc: "Get the number of the resource", - name: get_booking_businesses_count, - path: "/bookingBusinesses/$count" - ); + post!( + doc: "Create bookingBusiness", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "List bookingBusinesses", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: get_booking_businesses_count, + path: "/bookingBusinesses/$count" + ); } -impl BookingBusinessesIdApiClient { - api_client_link_id!(customer, CustomersIdApiClient); - api_client_link!(services, ServicesApiClient); - api_client_link_id!(service, ServicesIdApiClient); - api_client_link!(appointments, AppointmentsApiClient); - api_client_link_id!(custom_question, CustomQuestionsIdApiClient); - api_client_link!(custom_questions, CustomQuestionsApiClient); - api_client_link!(staff_members, StaffMembersApiClient); - api_client_link_id!(staff_member, StaffMembersIdApiClient); - api_client_link!(customers, CustomersApiClient); - api_client_link_id!(appointment, AppointmentsIdApiClient); +impl BookingBusinessesIdApiClient {api_client_link!(custom_questions, CustomQuestionsApiClient); +api_client_link_id!(customer, CustomersIdApiClient); +api_client_link_id!(custom_question, CustomQuestionsIdApiClient); +api_client_link!(services, ServicesApiClient); +api_client_link!(staff_members, StaffMembersApiClient); +api_client_link_id!(staff_member, StaffMembersIdApiClient); +api_client_link_id!(service, ServicesIdApiClient); +api_client_link_id!(appointment, AppointmentsIdApiClient); +api_client_link!(appointments, AppointmentsApiClient); +api_client_link!(customers, CustomersApiClient); - delete!( - doc: "Delete bookingBusiness", - name: delete_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - get!( - doc: "Get bookingBusiness", - name: get_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - patch!( - doc: "Update bookingbusiness", - name: update_booking_businesses, - path: "/bookingBusinesses/{{RID}}", - body: true - ); - post!( - doc: "Create new navigation property to calendarView for solutions", - name: create_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView", - body: true - ); - get!( - doc: "List business calendarView", - name: list_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView" - ); - get!( - doc: "Get the number of the resource", - name: calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/$count" - ); - delete!( - doc: "Delete navigation property calendarView for solutions", - name: delete_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - params: booking_appointment_id - ); - get!( - doc: "Get calendarView from solutions", - name: get_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - params: booking_appointment_id - ); - patch!( - doc: "Update the navigation property calendarView in solutions", - name: update_calendar_view, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}", - body: true, - params: booking_appointment_id - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/bookingBusinesses/{{RID}}/calendarView/{{id}}/cancel", - body: true, - params: booking_appointment_id - ); - post!( - doc: "Invoke action getStaffAvailability", - name: get_staff_availability, - path: "/bookingBusinesses/{{RID}}/getStaffAvailability", - body: true - ); - post!( - doc: "Invoke action publish", - name: publish, - path: "/bookingBusinesses/{{RID}}/publish" - ); - post!( - doc: "Invoke action unpublish", - name: unpublish, - path: "/bookingBusinesses/{{RID}}/unpublish" - ); + delete!( + doc: "Delete bookingBusiness", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusiness", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update bookingbusiness", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); } diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index fdadb442..0edfb1d1 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - CustomQuestionsApiClient, - CustomQuestionsIdApiClient, - ResourceIdentity::CustomQuestions -); +resource_api_client!(CustomQuestionsApiClient, CustomQuestionsIdApiClient, ResourceIdentity::CustomQuestions); impl CustomQuestionsApiClient { - post!( - doc: "Create bookingCustomQuestion", - name: create_custom_questions, - path: "/customQuestions", - body: true - ); - get!( - doc: "List customQuestions", - name: list_custom_questions, - path: "/customQuestions" - ); - get!( - doc: "Get the number of the resource", - name: get_custom_questions_count, - path: "/customQuestions/$count" - ); + post!( + doc: "Create bookingCustomQuestion", + name: create_custom_questions, + path: "/customQuestions", + body: true + ); + get!( + doc: "List customQuestions", + name: list_custom_questions, + path: "/customQuestions" + ); + get!( + doc: "Get the number of the resource", + name: get_custom_questions_count, + path: "/customQuestions/$count" + ); } impl CustomQuestionsIdApiClient { - delete!( - doc: "Delete bookingCustomQuestion", - name: delete_custom_questions, - path: "/customQuestions/{{RID}}" - ); - get!( - doc: "Get bookingCustomQuestion", - name: get_custom_questions, - path: "/customQuestions/{{RID}}" - ); - patch!( - doc: "Update bookingCustomQuestion", - name: update_custom_questions, - path: "/customQuestions/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingCustomQuestion", + name: delete_custom_questions, + path: "/customQuestions/{{RID}}" + ); + get!( + doc: "Get bookingCustomQuestion", + name: get_custom_questions, + path: "/customQuestions/{{RID}}" + ); + patch!( + doc: "Update bookingCustomQuestion", + name: update_custom_questions, + path: "/customQuestions/{{RID}}", + body: true + ); } diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index 3e798905..165c0482 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - CustomersApiClient, - CustomersIdApiClient, - ResourceIdentity::Customers -); +resource_api_client!(CustomersApiClient, CustomersIdApiClient, ResourceIdentity::Customers); impl CustomersApiClient { - post!( - doc: "Create bookingCustomer", - name: create_customers, - path: "/customers", - body: true - ); - get!( - doc: "List customers", - name: list_customers, - path: "/customers" - ); - get!( - doc: "Get the number of the resource", - name: get_customers_count, - path: "/customers/$count" - ); + post!( + doc: "Create bookingCustomer", + name: create_customers, + path: "/customers", + body: true + ); + get!( + doc: "List customers", + name: list_customers, + path: "/customers" + ); + get!( + doc: "Get the number of the resource", + name: get_customers_count, + path: "/customers/$count" + ); } impl CustomersIdApiClient { - delete!( - doc: "Delete bookingCustomer", - name: delete_customers, - path: "/customers/{{RID}}" - ); - get!( - doc: "Get bookingCustomer", - name: get_customers, - path: "/customers/{{RID}}" - ); - patch!( - doc: "Update bookingCustomer", - name: update_customers, - path: "/customers/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingCustomer", + name: delete_customers, + path: "/customers/{{RID}}" + ); + get!( + doc: "Get bookingCustomer", + name: get_customers, + path: "/customers/{{RID}}" + ); + patch!( + doc: "Update bookingCustomer", + name: update_customers, + path: "/customers/{{RID}}", + body: true + ); } diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index b4a42f77..d5f81a26 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,15 +1,15 @@ -mod appointments; +mod request; mod booking_businesses; +mod appointments; +mod services; mod custom_questions; mod customers; -mod request; -mod services; mod staff_members; -pub use appointments::*; +pub use request::*; pub use booking_businesses::*; +pub use appointments::*; +pub use services::*; pub use custom_questions::*; pub use customers::*; -pub use request::*; -pub use services::*; pub use staff_members::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index 8a3cfc46..d8ef6951 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,19 +5,18 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient { - api_client_link_id!(booking_business, BookingBusinessesIdApiClient); - api_client_link!(booking_businesses, BookingBusinessesApiClient); +impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); +api_client_link!(booking_businesses, BookingBusinessesApiClient); - get!( - doc: "Get solutions", - name: get_solutions_root, - path: "/solutions" - ); - patch!( - doc: "Update solutions", - name: update_solutions_root, - path: "/solutions", - body: true - ); + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); } diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index db496cd9..2411df25 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - ServicesApiClient, - ServicesIdApiClient, - ResourceIdentity::Services -); +resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); impl ServicesApiClient { - post!( - doc: "Create bookingService", - name: create_services, - path: "/services", - body: true - ); - get!( - doc: "List services", - name: list_services, - path: "/services" - ); - get!( - doc: "Get the number of the resource", - name: get_services_count, - path: "/services/$count" - ); + post!( + doc: "Create bookingService", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "List services", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: get_services_count, + path: "/services/$count" + ); } impl ServicesIdApiClient { - delete!( - doc: "Delete bookingService", - name: delete_services, - path: "/services/{{RID}}" - ); - get!( - doc: "Get bookingService", - name: get_services, - path: "/services/{{RID}}" - ); - patch!( - doc: "Update bookingservice", - name: update_services, - path: "/services/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingService", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get bookingService", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update bookingservice", + name: update_services, + path: "/services/{{RID}}", + body: true + ); } diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index e3310d61..edb6e3c1 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -2,46 +2,42 @@ use crate::api_default_imports::*; -resource_api_client!( - StaffMembersApiClient, - StaffMembersIdApiClient, - ResourceIdentity::StaffMembers -); +resource_api_client!(StaffMembersApiClient, StaffMembersIdApiClient, ResourceIdentity::StaffMembers); impl StaffMembersApiClient { - post!( - doc: "Create bookingStaffMember", - name: create_staff_members, - path: "/staffMembers", - body: true - ); - get!( - doc: "List staffMembers", - name: list_staff_members, - path: "/staffMembers" - ); - get!( - doc: "Get the number of the resource", - name: staff_members, - path: "/staffMembers/$count" - ); + post!( + doc: "Create bookingStaffMember", + name: create_staff_members, + path: "/staffMembers", + body: true + ); + get!( + doc: "List staffMembers", + name: list_staff_members, + path: "/staffMembers" + ); + get!( + doc: "Get the number of the resource", + name: staff_members, + path: "/staffMembers/$count" + ); } impl StaffMembersIdApiClient { - delete!( - doc: "Delete bookingStaffMember", - name: delete_staff_members, - path: "/staffMembers/{{RID}}" - ); - get!( - doc: "Get bookingStaffMember", - name: get_staff_members, - path: "/staffMembers/{{RID}}" - ); - patch!( - doc: "Update bookingstaffmember", - name: update_staff_members, - path: "/staffMembers/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingStaffMember", + name: delete_staff_members, + path: "/staffMembers/{{RID}}" + ); + get!( + doc: "Get bookingStaffMember", + name: get_staff_members, + path: "/staffMembers/{{RID}}" + ); + patch!( + doc: "Update bookingstaffmember", + name: update_staff_members, + path: "/staffMembers/{{RID}}", + body: true + ); } From 342066c7a1c80c544bcbe986a5ebdc5c0e460f23 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Tue, 28 May 2024 09:27:16 +0100 Subject: [PATCH 107/118] fix: add solutions back to lib & client --- src/client/graph.rs | 4 ++-- src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/graph.rs b/src/client/graph.rs index ba84a7f1..2e409d14 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -56,7 +56,7 @@ use crate::reports::ReportsApiClient; use crate::schema_extensions::{SchemaExtensionsApiClient, SchemaExtensionsIdApiClient}; use crate::service_principals::{ServicePrincipalsApiClient, ServicePrincipalsIdApiClient}; use crate::sites::{SitesApiClient, SitesIdApiClient}; -//use crate::solutions::SolutionsApiClient; +use crate::solutions::SolutionsApiClient; use crate::subscribed_skus::SubscribedSkusApiClient; use crate::subscriptions::{SubscriptionsApiClient, SubscriptionsIdApiClient}; use crate::teams::{TeamsApiClient, TeamsIdApiClient}; @@ -379,7 +379,7 @@ impl Graph { api_client_impl!(sites, SitesApiClient, site, SitesIdApiClient); - //api_client_impl!(solutions, SolutionsApiClient); + api_client_impl!(solutions, SolutionsApiClient); api_client_impl!( subscribed_skus, diff --git a/src/lib.rs b/src/lib.rs index 9fcb42d9..e02d1ea3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ pub mod reports; pub mod schema_extensions; pub mod service_principals; pub mod sites; -//pub mod solutions; +pub mod solutions; pub mod subscribed_skus; pub mod subscriptions; pub mod teams; From e7139b49cc76cd93b49684981348ee0e0091e329 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Tue, 28 May 2024 09:37:00 +0100 Subject: [PATCH 108/118] fix: move import and linkup to correct method - should be in the `new` method matching on `ResourceIdentity`, not under `get_write_configuration` --- .../src/settings/resource_settings.rs | 13 +- src/solutions/appointments/request.rs | 82 ++++++------ src/solutions/booking_businesses/request.rs | 126 ++++++++++-------- src/solutions/custom_questions/request.rs | 70 +++++----- src/solutions/customers/request.rs | 70 +++++----- src/solutions/mod.rs | 12 +- src/solutions/request.rs | 27 ++-- src/solutions/services/request.rs | 70 +++++----- src/solutions/staff_members/request.rs | 70 +++++----- 9 files changed, 281 insertions(+), 259 deletions(-) diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 3174d0a5..3b976c7b 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1198,7 +1198,7 @@ impl ResourceSettings { .build() .unwrap(), ResourceIdentity::BookingBusinesses => ResourceSettings::builder(path_name, ri) - .imports(vec!["crate::solutions::*"]) + .imports(vec!["crate::solutions::*", "crate::users::*"]) .api_client_links(vec![ ApiClientLinkSettings(Some("BookingBusinessesIdApiClient"), vec![ @@ -1212,6 +1212,8 @@ impl ResourceSettings { ApiClientLink::StructId("customer", "CustomersIdApiClient"), ApiClientLink::Struct("staff_members", "StaffMembersApiClient"), ApiClientLink::StructId("staff_member", "StaffMembersIdApiClient"), + ApiClientLink::Struct("calendar_views", "CalendarViewApiClient"), + ApiClientLink::StructId("calendar_view", "CalendarViewIdApiClient"), ] ) ]) @@ -2703,17 +2705,8 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf .build() .unwrap(), ResourceIdentity::BookingBusinesses => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) - // look at ~ 2506 .trim_path_start("/solutions") .filter_path(vec!["appointments", "calendarView", "customQuestions", "customers", "services", "staffMembers"]) - .imports(vec!["crate::users::*"]) - .api_client_links(vec![ - ApiClientLinkSettings(Some("BookingBusinessesIdApiClient"), vec![ - //ApiClientLinkSettings(None, vec![ - ApiClientLink::Struct("calendar_views", "CalendarViewApiClient"), - ApiClientLink::StructId("calendar_view", "CalendarViewIdApiClient"), - ]) - ]) .build() .unwrap(), ResourceIdentity::Appointments => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index 05ba48d8..e4c12853 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -2,48 +2,52 @@ use crate::api_default_imports::*; -resource_api_client!(AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments); +resource_api_client!( + AppointmentsApiClient, + AppointmentsIdApiClient, + ResourceIdentity::Appointments +); impl AppointmentsApiClient { - post!( - doc: "Create bookingAppointment", - name: create_appointments, - path: "/appointments", - body: true - ); - get!( - doc: "List appointments", - name: list_appointments, - path: "/appointments" - ); - get!( - doc: "Get the number of the resource", - name: get_appointments_count, - path: "/appointments/$count" - ); + post!( + doc: "Create bookingAppointment", + name: create_appointments, + path: "/appointments", + body: true + ); + get!( + doc: "List appointments", + name: list_appointments, + path: "/appointments" + ); + get!( + doc: "Get the number of the resource", + name: get_appointments_count, + path: "/appointments/$count" + ); } impl AppointmentsIdApiClient { - delete!( - doc: "Delete bookingAppointment", - name: delete_appointments, - path: "/appointments/{{RID}}" - ); - get!( - doc: "Get bookingAppointment", - name: get_appointments, - path: "/appointments/{{RID}}" - ); - patch!( - doc: "Update bookingAppointment", - name: update_appointments, - path: "/appointments/{{RID}}", - body: true - ); - post!( - doc: "Invoke action cancel", - name: cancel, - path: "/appointments/{{RID}}/cancel", - body: true - ); + delete!( + doc: "Delete bookingAppointment", + name: delete_appointments, + path: "/appointments/{{RID}}" + ); + get!( + doc: "Get bookingAppointment", + name: get_appointments, + path: "/appointments/{{RID}}" + ); + patch!( + doc: "Update bookingAppointment", + name: update_appointments, + path: "/appointments/{{RID}}", + body: true + ); + post!( + doc: "Invoke action cancel", + name: cancel, + path: "/appointments/{{RID}}/cancel", + body: true + ); } diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index 019262b4..8e6eedb6 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -2,69 +2,77 @@ use crate::api_default_imports::*; use crate::solutions::*; +use crate::users::*; -resource_api_client!(BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses); +resource_api_client!( + BookingBusinessesApiClient, + BookingBusinessesIdApiClient, + ResourceIdentity::BookingBusinesses +); impl BookingBusinessesApiClient { - post!( - doc: "Create bookingBusiness", - name: create_booking_businesses, - path: "/bookingBusinesses", - body: true - ); - get!( - doc: "List bookingBusinesses", - name: list_booking_businesses, - path: "/bookingBusinesses" - ); - get!( - doc: "Get the number of the resource", - name: get_booking_businesses_count, - path: "/bookingBusinesses/$count" - ); + post!( + doc: "Create bookingBusiness", + name: create_booking_businesses, + path: "/bookingBusinesses", + body: true + ); + get!( + doc: "List bookingBusinesses", + name: list_booking_businesses, + path: "/bookingBusinesses" + ); + get!( + doc: "Get the number of the resource", + name: get_booking_businesses_count, + path: "/bookingBusinesses/$count" + ); } -impl BookingBusinessesIdApiClient {api_client_link!(custom_questions, CustomQuestionsApiClient); -api_client_link_id!(customer, CustomersIdApiClient); -api_client_link_id!(custom_question, CustomQuestionsIdApiClient); -api_client_link!(services, ServicesApiClient); -api_client_link!(staff_members, StaffMembersApiClient); -api_client_link_id!(staff_member, StaffMembersIdApiClient); -api_client_link_id!(service, ServicesIdApiClient); -api_client_link_id!(appointment, AppointmentsIdApiClient); -api_client_link!(appointments, AppointmentsApiClient); -api_client_link!(customers, CustomersApiClient); +impl BookingBusinessesIdApiClient { + api_client_link_id!(service, ServicesIdApiClient); + api_client_link!(customers, CustomersApiClient); + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link!(staff_members, StaffMembersApiClient); + api_client_link_id!(appointment, AppointmentsIdApiClient); + api_client_link_id!(customer, CustomersIdApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); + api_client_link!(appointments, AppointmentsApiClient); + api_client_link!(calendar_views, CalendarViewApiClient); + api_client_link!(services, ServicesApiClient); + api_client_link_id!(calendar_view, CalendarViewIdApiClient); + api_client_link_id!(custom_question, CustomQuestionsIdApiClient); - delete!( - doc: "Delete bookingBusiness", - name: delete_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - get!( - doc: "Get bookingBusiness", - name: get_booking_businesses, - path: "/bookingBusinesses/{{RID}}" - ); - patch!( - doc: "Update bookingbusiness", - name: update_booking_businesses, - path: "/bookingBusinesses/{{RID}}", - body: true - ); - post!( - doc: "Invoke action getStaffAvailability", - name: get_staff_availability, - path: "/bookingBusinesses/{{RID}}/getStaffAvailability", - body: true - ); - post!( - doc: "Invoke action publish", - name: publish, - path: "/bookingBusinesses/{{RID}}/publish" - ); - post!( - doc: "Invoke action unpublish", - name: unpublish, - path: "/bookingBusinesses/{{RID}}/unpublish" - ); + delete!( + doc: "Delete bookingBusiness", + name: delete_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + get!( + doc: "Get bookingBusiness", + name: get_booking_businesses, + path: "/bookingBusinesses/{{RID}}" + ); + patch!( + doc: "Update bookingbusiness", + name: update_booking_businesses, + path: "/bookingBusinesses/{{RID}}", + body: true + ); + post!( + doc: "Invoke action getStaffAvailability", + name: get_staff_availability, + path: "/bookingBusinesses/{{RID}}/getStaffAvailability", + body: true + ); + post!( + doc: "Invoke action publish", + name: publish, + path: "/bookingBusinesses/{{RID}}/publish" + ); + post!( + doc: "Invoke action unpublish", + name: unpublish, + path: "/bookingBusinesses/{{RID}}/unpublish" + ); } diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index 0edfb1d1..fdadb442 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(CustomQuestionsApiClient, CustomQuestionsIdApiClient, ResourceIdentity::CustomQuestions); +resource_api_client!( + CustomQuestionsApiClient, + CustomQuestionsIdApiClient, + ResourceIdentity::CustomQuestions +); impl CustomQuestionsApiClient { - post!( - doc: "Create bookingCustomQuestion", - name: create_custom_questions, - path: "/customQuestions", - body: true - ); - get!( - doc: "List customQuestions", - name: list_custom_questions, - path: "/customQuestions" - ); - get!( - doc: "Get the number of the resource", - name: get_custom_questions_count, - path: "/customQuestions/$count" - ); + post!( + doc: "Create bookingCustomQuestion", + name: create_custom_questions, + path: "/customQuestions", + body: true + ); + get!( + doc: "List customQuestions", + name: list_custom_questions, + path: "/customQuestions" + ); + get!( + doc: "Get the number of the resource", + name: get_custom_questions_count, + path: "/customQuestions/$count" + ); } impl CustomQuestionsIdApiClient { - delete!( - doc: "Delete bookingCustomQuestion", - name: delete_custom_questions, - path: "/customQuestions/{{RID}}" - ); - get!( - doc: "Get bookingCustomQuestion", - name: get_custom_questions, - path: "/customQuestions/{{RID}}" - ); - patch!( - doc: "Update bookingCustomQuestion", - name: update_custom_questions, - path: "/customQuestions/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingCustomQuestion", + name: delete_custom_questions, + path: "/customQuestions/{{RID}}" + ); + get!( + doc: "Get bookingCustomQuestion", + name: get_custom_questions, + path: "/customQuestions/{{RID}}" + ); + patch!( + doc: "Update bookingCustomQuestion", + name: update_custom_questions, + path: "/customQuestions/{{RID}}", + body: true + ); } diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index 165c0482..3e798905 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(CustomersApiClient, CustomersIdApiClient, ResourceIdentity::Customers); +resource_api_client!( + CustomersApiClient, + CustomersIdApiClient, + ResourceIdentity::Customers +); impl CustomersApiClient { - post!( - doc: "Create bookingCustomer", - name: create_customers, - path: "/customers", - body: true - ); - get!( - doc: "List customers", - name: list_customers, - path: "/customers" - ); - get!( - doc: "Get the number of the resource", - name: get_customers_count, - path: "/customers/$count" - ); + post!( + doc: "Create bookingCustomer", + name: create_customers, + path: "/customers", + body: true + ); + get!( + doc: "List customers", + name: list_customers, + path: "/customers" + ); + get!( + doc: "Get the number of the resource", + name: get_customers_count, + path: "/customers/$count" + ); } impl CustomersIdApiClient { - delete!( - doc: "Delete bookingCustomer", - name: delete_customers, - path: "/customers/{{RID}}" - ); - get!( - doc: "Get bookingCustomer", - name: get_customers, - path: "/customers/{{RID}}" - ); - patch!( - doc: "Update bookingCustomer", - name: update_customers, - path: "/customers/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingCustomer", + name: delete_customers, + path: "/customers/{{RID}}" + ); + get!( + doc: "Get bookingCustomer", + name: get_customers, + path: "/customers/{{RID}}" + ); + patch!( + doc: "Update bookingCustomer", + name: update_customers, + path: "/customers/{{RID}}", + body: true + ); } diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index d5f81a26..b4a42f77 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -1,15 +1,15 @@ -mod request; -mod booking_businesses; mod appointments; -mod services; +mod booking_businesses; mod custom_questions; mod customers; +mod request; +mod services; mod staff_members; -pub use request::*; -pub use booking_businesses::*; pub use appointments::*; -pub use services::*; +pub use booking_businesses::*; pub use custom_questions::*; pub use customers::*; +pub use request::*; +pub use services::*; pub use staff_members::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index d8ef6951..5e4314c0 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -5,18 +5,19 @@ use crate::solutions::*; resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); -impl SolutionsApiClient {api_client_link_id!(booking_business, BookingBusinessesIdApiClient); -api_client_link!(booking_businesses, BookingBusinessesApiClient); +impl SolutionsApiClient { + api_client_link!(booking_businesses, BookingBusinessesApiClient); + api_client_link_id!(booking_business, BookingBusinessesIdApiClient); - get!( - doc: "Get solutions", - name: get_solutions_root, - path: "/solutions" - ); - patch!( - doc: "Update solutions", - name: update_solutions_root, - path: "/solutions", - body: true - ); + get!( + doc: "Get solutions", + name: get_solutions_root, + path: "/solutions" + ); + patch!( + doc: "Update solutions", + name: update_solutions_root, + path: "/solutions", + body: true + ); } diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index 2411df25..db496cd9 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services); +resource_api_client!( + ServicesApiClient, + ServicesIdApiClient, + ResourceIdentity::Services +); impl ServicesApiClient { - post!( - doc: "Create bookingService", - name: create_services, - path: "/services", - body: true - ); - get!( - doc: "List services", - name: list_services, - path: "/services" - ); - get!( - doc: "Get the number of the resource", - name: get_services_count, - path: "/services/$count" - ); + post!( + doc: "Create bookingService", + name: create_services, + path: "/services", + body: true + ); + get!( + doc: "List services", + name: list_services, + path: "/services" + ); + get!( + doc: "Get the number of the resource", + name: get_services_count, + path: "/services/$count" + ); } impl ServicesIdApiClient { - delete!( - doc: "Delete bookingService", - name: delete_services, - path: "/services/{{RID}}" - ); - get!( - doc: "Get bookingService", - name: get_services, - path: "/services/{{RID}}" - ); - patch!( - doc: "Update bookingservice", - name: update_services, - path: "/services/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingService", + name: delete_services, + path: "/services/{{RID}}" + ); + get!( + doc: "Get bookingService", + name: get_services, + path: "/services/{{RID}}" + ); + patch!( + doc: "Update bookingservice", + name: update_services, + path: "/services/{{RID}}", + body: true + ); } diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index edb6e3c1..e3310d61 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -2,42 +2,46 @@ use crate::api_default_imports::*; -resource_api_client!(StaffMembersApiClient, StaffMembersIdApiClient, ResourceIdentity::StaffMembers); +resource_api_client!( + StaffMembersApiClient, + StaffMembersIdApiClient, + ResourceIdentity::StaffMembers +); impl StaffMembersApiClient { - post!( - doc: "Create bookingStaffMember", - name: create_staff_members, - path: "/staffMembers", - body: true - ); - get!( - doc: "List staffMembers", - name: list_staff_members, - path: "/staffMembers" - ); - get!( - doc: "Get the number of the resource", - name: staff_members, - path: "/staffMembers/$count" - ); + post!( + doc: "Create bookingStaffMember", + name: create_staff_members, + path: "/staffMembers", + body: true + ); + get!( + doc: "List staffMembers", + name: list_staff_members, + path: "/staffMembers" + ); + get!( + doc: "Get the number of the resource", + name: staff_members, + path: "/staffMembers/$count" + ); } impl StaffMembersIdApiClient { - delete!( - doc: "Delete bookingStaffMember", - name: delete_staff_members, - path: "/staffMembers/{{RID}}" - ); - get!( - doc: "Get bookingStaffMember", - name: get_staff_members, - path: "/staffMembers/{{RID}}" - ); - patch!( - doc: "Update bookingstaffmember", - name: update_staff_members, - path: "/staffMembers/{{RID}}", - body: true - ); + delete!( + doc: "Delete bookingStaffMember", + name: delete_staff_members, + path: "/staffMembers/{{RID}}" + ); + get!( + doc: "Get bookingStaffMember", + name: get_staff_members, + path: "/staffMembers/{{RID}}" + ); + patch!( + doc: "Update bookingstaffmember", + name: update_staff_members, + path: "/staffMembers/{{RID}}", + body: true + ); } From 7f09c0845497d890c7e51de205e93317e1cf826e Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 31 May 2024 14:49:54 +0100 Subject: [PATCH 109/118] fix: replace with --- graph-codegen/src/macros/macro_queue_writer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph-codegen/src/macros/macro_queue_writer.rs b/graph-codegen/src/macros/macro_queue_writer.rs index 3d2d5c29..7465064e 100644 --- a/graph-codegen/src/macros/macro_queue_writer.rs +++ b/graph-codegen/src/macros/macro_queue_writer.rs @@ -448,7 +448,7 @@ pub trait MacroImplWriter { } let resource_api_client_impl = format!( - "resource_api_client!({}, {});\n", + "api_client!({}, {});\n", client_impl_string, settings[0].ri.enum_string() ); From b8ce31d42171fe4f4fa482b280546e0be95b5c88 Mon Sep 17 00:00:00 2001 From: Mike P <buhaytza2005@gmail.com> Date: Fri, 31 May 2024 14:52:30 +0100 Subject: [PATCH 110/118] fix: manual replacement of in solutions folder --- src/solutions/appointments/request.rs | 2 +- src/solutions/booking_businesses/request.rs | 2 +- src/solutions/custom_questions/request.rs | 2 +- src/solutions/customers/request.rs | 2 +- src/solutions/request.rs | 2 +- src/solutions/services/request.rs | 2 +- src/solutions/staff_members/request.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/solutions/appointments/request.rs b/src/solutions/appointments/request.rs index e4c12853..a9efb94c 100644 --- a/src/solutions/appointments/request.rs +++ b/src/solutions/appointments/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( AppointmentsApiClient, AppointmentsIdApiClient, ResourceIdentity::Appointments diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index 8e6eedb6..c9b49854 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -4,7 +4,7 @@ use crate::api_default_imports::*; use crate::solutions::*; use crate::users::*; -resource_api_client!( +api_client!( BookingBusinessesApiClient, BookingBusinessesIdApiClient, ResourceIdentity::BookingBusinesses diff --git a/src/solutions/custom_questions/request.rs b/src/solutions/custom_questions/request.rs index fdadb442..15042457 100644 --- a/src/solutions/custom_questions/request.rs +++ b/src/solutions/custom_questions/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( CustomQuestionsApiClient, CustomQuestionsIdApiClient, ResourceIdentity::CustomQuestions diff --git a/src/solutions/customers/request.rs b/src/solutions/customers/request.rs index 3e798905..b306fe26 100644 --- a/src/solutions/customers/request.rs +++ b/src/solutions/customers/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( CustomersApiClient, CustomersIdApiClient, ResourceIdentity::Customers diff --git a/src/solutions/request.rs b/src/solutions/request.rs index 5e4314c0..2e4a593c 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -3,7 +3,7 @@ use crate::api_default_imports::*; use crate::solutions::*; -resource_api_client!(SolutionsApiClient, ResourceIdentity::Solutions); +api_client!(SolutionsApiClient, ResourceIdentity::Solutions); impl SolutionsApiClient { api_client_link!(booking_businesses, BookingBusinessesApiClient); diff --git a/src/solutions/services/request.rs b/src/solutions/services/request.rs index db496cd9..8bf195cf 100644 --- a/src/solutions/services/request.rs +++ b/src/solutions/services/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( ServicesApiClient, ServicesIdApiClient, ResourceIdentity::Services diff --git a/src/solutions/staff_members/request.rs b/src/solutions/staff_members/request.rs index e3310d61..73b5775e 100644 --- a/src/solutions/staff_members/request.rs +++ b/src/solutions/staff_members/request.rs @@ -2,7 +2,7 @@ use crate::api_default_imports::*; -resource_api_client!( +api_client!( StaffMembersApiClient, StaffMembersIdApiClient, ResourceIdentity::StaffMembers From 197ee03a467ab7607abf8cdfcb667bff324354d7 Mon Sep 17 00:00:00 2001 From: Mike Potapenco <buhaytza2005@gmail.com> Date: Fri, 31 May 2024 17:51:57 +0100 Subject: [PATCH 111/118] docs: Replace references to Graph with GraphClient - as per #473 - Readme - Examples --- README.md | 66 +++++++++---------- examples/drive/worksheet.rs | 4 +- examples/identity_platform_auth/README.md | 6 +- .../client_credentials/mod.rs | 2 +- .../mail_folders_and_messages/messages.rs | 2 +- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index e5b936bd..cfad213d 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ use graph_rs_sdk::*; #[tokio::main] async fn main() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .users() @@ -155,11 +155,11 @@ use `tokio` when using the blocking client. graph-rs-sdk = "2.0.0" #### Example +```rust use graph_rs_sdk::*; -```rust fn main() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .users() @@ -196,7 +196,7 @@ The send() method is the main method for sending a request and returns a `Result use graph_rs_sdk::*; pub async fn get_drive_item() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .me() @@ -229,7 +229,7 @@ use std::error::Error; #[tokio::main] async fn main() -> Result<(), Box<dyn Error>> { - let client = Graph::new("token"); + let client = GraphClient::new("token"); let response = client.users().list_user().send().await?; if !response.status().is_success() { @@ -268,7 +268,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static ITEM_ID: &str = "ITEM_ID"; pub async fn get_drive_item() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let drive_item = DriveItem { id: None, @@ -348,7 +348,7 @@ pub struct Users { } async fn paging() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let deque = client .users() @@ -378,7 +378,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; pub async fn stream_next_links() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut stream = client .users() @@ -399,7 +399,7 @@ pub async fn stream_next_links() -> GraphResult<()> { } pub async fn stream_delta() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut stream = client .users() .delta() @@ -426,7 +426,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; async fn channel_next_links() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let mut receiver = client .users() .list_user() @@ -464,7 +464,7 @@ users, and groups. use graph_rs_sdk::*; async fn drives() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .drives() @@ -497,7 +497,7 @@ async fn drives() -> GraphResult<()> { #### Me API ```rust async fn drive_me() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .me() @@ -520,7 +520,7 @@ async fn drive_me() -> GraphResult<()> { #### Users API ```RUST async fn drive_users() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .user("USER_ID") @@ -542,7 +542,7 @@ async fn drive_users() -> GraphResult<()> { #### Sites API ```RUST async fn drive_users() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .site("SITE_ID") @@ -575,7 +575,7 @@ static PARENT_ID: &str = "PARENT_ID"; // https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online pub async fn create_new_folder() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let folder: HashMap<String, serde_json::Value> = HashMap::new(); let response = client @@ -604,7 +604,7 @@ Path based addressing for drive. // Start the path with :/ and end with : async fn get_item_by_path() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let response = client .me() @@ -631,7 +631,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; async fn get_mail_folder() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client.me() .mail_folder(MAIL_FOLDER_ID) @@ -656,7 +656,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static MAIL_FOLDER_ID: &str = "MAIL_FOLDER_ID"; async fn create_message() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -694,7 +694,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; async fn send_mail() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -741,7 +741,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static MAIL_FOLDER_ID: &str = "MAIL_FOLDER_ID"; async fn create_mail_folder_message() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() @@ -778,7 +778,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static USER_ID: &str = "USER_ID"; async fn get_user_inbox_messages() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) @@ -829,7 +829,7 @@ struct EmailAddress { } async fn create_message() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); let mut body: HashMap<String, String> = HashMap::new(); body.insert("contentType".to_string(), "HTML".to_string()); @@ -870,7 +870,7 @@ async fn create_message() -> GraphResult<()> { use graph_rs_sdk::*; async fn create_message() -> GraphResult<()> { - let client = Graph::new("ACCESS_TOKEN"); + let client = GraphClient::new("ACCESS_TOKEN"); // Get all files in the root of the drive // and select only specific properties. @@ -903,7 +903,7 @@ static USER_ID: &str = "USER_ID"; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; async fn batch() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let json = serde_json::json!({ "requests": [ @@ -966,7 +966,7 @@ use graph_rs_sdk::*; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; async fn list_users() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .users() @@ -996,7 +996,7 @@ static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static USER_ID: &str = "USER_ID"; async fn get_user() -> GraphResult<()> { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .user(USER_ID) @@ -1045,7 +1045,7 @@ Once you have built a `ConfidentialClientApplication` or a `PublicClientApplicat you can pass these to the graph client. Automatic token refresh is also done by passing the `ConfidentialClientApplication` or the -`PublicClientApplication` to the `Graph` client. +`PublicClientApplication` to the `GraphClient` client. For more extensive examples see the [OAuth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/oauth) in the examples/oauth @@ -1090,7 +1090,7 @@ will perform the request to get an access token on the first graph api call that ```rust use graph_rs_sdk::{ - Graph, + GraphClient, oauth::ConfidentialClientApplication, }; @@ -1108,7 +1108,7 @@ async fn build_client( .with_redirect_uri(redirect_uri) .build(); - let graph_client = Graph::from(confidential_client); + let graph_client = GraphClient::from(confidential_client); Ok(graph_client) } @@ -1172,16 +1172,16 @@ as an administrator see [Admin Consent Example](https://github.com/sreeise/graph ```rust use graph_rs_sdk::{ - oauth::ConfidentialClientApplication, Graph + oauth::ConfidentialClientApplication, GraphClient }; -pub async fn get_graph_client(tenant: &str, client_id: &str, client_secret: &str) -> Graph { +pub async fn get_graph_client(tenant: &str, client_id: &str, client_secret: &str) -> GraphClient { let mut confidential_client_application = ConfidentialClientApplication::builder(client_id) .with_client_secret(client_secret) .with_tenant(tenant) .build(); - Graph::from(&confidential_client_application) + GraphClient::from(&confidential_client_application) } ``` @@ -1228,7 +1228,7 @@ Using automatic token refresh requires getting a refresh token as part of the to To get a refresh token you must include the `offline_access` scope. Automatic token refresh is done by passing the `ConfidentialClientApplication` or the -`PublicClientApplication` to the `Graph` client. +`PublicClientApplication` to the `GraphClient` client. If you are using the `client credentials` grant you do not need the `offline_access` scope. Tokens will still be automatically refreshed as this flow does not require using a refresh token to get diff --git a/examples/drive/worksheet.rs b/examples/drive/worksheet.rs index 0d6290ae..21a9d4d2 100644 --- a/examples/drive/worksheet.rs +++ b/examples/drive/worksheet.rs @@ -1,12 +1,12 @@ use graph_rs_sdk::http::Body; -use graph_rs_sdk::Graph; +use graph_rs_sdk::GraphClient; static ACCESS_TOKEN: &str = "ACCESS_TOKEN"; static DRIVE_ID: &str = "DRIVE_ID"; static ITEM_ID: &str = "ITEM_ID"; pub async fn update_range_by_address() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); //update single cell let range_address = "A1"; diff --git a/examples/identity_platform_auth/README.md b/examples/identity_platform_auth/README.md index 0c50e950..33a7c98d 100644 --- a/examples/identity_platform_auth/README.md +++ b/examples/identity_platform_auth/README.md @@ -33,7 +33,7 @@ documentation from Microsoft. ```rust use graph_rs_sdk::{ - Graph, + GraphClient, oauth::ConfidentialClientApplication, }; @@ -51,7 +51,7 @@ async fn build_client( .with_redirect_uri(redirect_uri)? .build(); - let graph_client = Graph::from(confidential_client); + let graph_client = GraphClient::from(confidential_client); Ok(graph_client) } @@ -68,7 +68,7 @@ documentation from Microsoft. ```rust use graph_rs_sdk::{ - Graph, + GraphClient, oauth::ConfidentialClientApplication, }; diff --git a/examples/identity_platform_auth/client_credentials/mod.rs b/examples/identity_platform_auth/client_credentials/mod.rs index 1e8fd53a..54897e87 100644 --- a/examples/identity_platform_auth/client_credentials/mod.rs +++ b/examples/identity_platform_auth/client_credentials/mod.rs @@ -13,4 +13,4 @@ mod client_credentials_secret; mod server_examples; -use graph_rs_sdk::{identity::ConfidentialClientApplication, Graph}; +use graph_rs_sdk::{identity::ConfidentialClientApplication, GraphClient}; diff --git a/examples/mail_folders_and_messages/messages.rs b/examples/mail_folders_and_messages/messages.rs index 05732d2c..658ebaf2 100644 --- a/examples/mail_folders_and_messages/messages.rs +++ b/examples/mail_folders_and_messages/messages.rs @@ -95,7 +95,7 @@ pub async fn update_message() { } pub async fn send_message() { - let client = Graph::new(ACCESS_TOKEN); + let client = GraphClient::new(ACCESS_TOKEN); let response = client .me() From c11aa92f30667585cb97b70d44ca31c8e86d110f Mon Sep 17 00:00:00 2001 From: Simon <smndtrl@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:54:09 +0200 Subject: [PATCH 112/118] Update Cargo.toml --- graph-core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index 1f3cc129..cc6a8d8e 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -19,7 +19,7 @@ jsonwebtoken = "9.1.0" parking_lot = "0.12.1" percent-encoding = "2" reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } -ring = "0.16.15" +ring = "0.17" serde = { version = "1", features = ["derive"] } serde_json = "1" strum = { version = "0.25.0", features = ["derive"] } From 75450e364b88a056400b5260dfaeadfd0164311c Mon Sep 17 00:00:00 2001 From: Simon <smndtrl@users.noreply.github.com> Date: Mon, 3 Jun 2024 15:54:24 +0200 Subject: [PATCH 113/118] Update Cargo.toml --- graph-error/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 55dbefa2..475d4fd4 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -20,7 +20,7 @@ http-serde = "1" http = "0.2.11" jsonwebtoken = "9.1.0" reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } -ring = "0.16.15" +ring = "0.17" serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "1" From d1084f8b5b68054c3a349279f412c1b3425a528e Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 8 Jun 2024 02:03:37 -0400 Subject: [PATCH 114/118] Fix issue with OpenAPI Response modal and add virutal events to solutions --- .../src/macros/macro_queue_writer.rs | 17 +-- graph-codegen/src/openapi/response.rs | 5 +- .../src/settings/method_macro_modifier.rs | 27 ++++ .../src/settings/resource_settings.rs | 62 ++++++++- graph-core/src/resource/resource_identity.rs | 8 ++ src/events/mod.rs | 3 + src/events/request.rs | 45 +++++++ src/solutions/booking_businesses/request.rs | 16 +-- src/solutions/mod.rs | 8 ++ src/solutions/request.rs | 1 + src/solutions/virtual_events/mod.rs | 3 + src/solutions/virtual_events/request.rs | 30 +++++ src/solutions/virtual_events_events/mod.rs | 3 + .../virtual_events_events/request.rs | 51 ++++++++ src/solutions/virtual_events_sessions/mod.rs | 3 + .../virtual_events_sessions/request.rs | 120 ++++++++++++++++++ src/solutions/virtual_events_webinars/mod.rs | 3 + .../virtual_events_webinars/request.rs | 98 ++++++++++++++ 18 files changed, 483 insertions(+), 20 deletions(-) create mode 100644 src/events/mod.rs create mode 100644 src/events/request.rs create mode 100644 src/solutions/virtual_events/mod.rs create mode 100644 src/solutions/virtual_events/request.rs create mode 100644 src/solutions/virtual_events_events/mod.rs create mode 100644 src/solutions/virtual_events_events/request.rs create mode 100644 src/solutions/virtual_events_sessions/mod.rs create mode 100644 src/solutions/virtual_events_sessions/request.rs create mode 100644 src/solutions/virtual_events_webinars/mod.rs create mode 100644 src/solutions/virtual_events_webinars/request.rs diff --git a/graph-codegen/src/macros/macro_queue_writer.rs b/graph-codegen/src/macros/macro_queue_writer.rs index 7465064e..961b31ae 100644 --- a/graph-codegen/src/macros/macro_queue_writer.rs +++ b/graph-codegen/src/macros/macro_queue_writer.rs @@ -24,14 +24,11 @@ use std::str::FromStr; /// /// # Example Macro /// ```rust,ignore -/// get!({ -/// doc: "# Get historyItems from me", -/// name: get_activity_history, -/// response: serde_json::Value, -/// path: "/activities/{{id}}/historyItems/{{id1}}}", -/// params: [ user_activity_id history_items_id ], -/// has_body: false -/// }); +/// get!( +/// doc: "Get solutions", +/// name: get_solutions_root, +/// path: "/solutions" +/// ); /// ``` pub trait MacroQueueWriter { type Metadata: Metadata; @@ -447,12 +444,12 @@ pub trait MacroImplWriter { )); } - let resource_api_client_impl = format!( + let api_client_impl = format!( "api_client!({}, {});\n", client_impl_string, settings[0].ri.enum_string() ); - buf.put(resource_api_client_impl.as_bytes()); + buf.put(api_client_impl.as_bytes()); for (name, path_metadata_queue) in path_metadata_map.iter() { let api_client_name = format!("{name}ApiClient"); diff --git a/graph-codegen/src/openapi/response.rs b/graph-codegen/src/openapi/response.rs index 57e9a14b..34bba728 100644 --- a/graph-codegen/src/openapi/response.rs +++ b/graph-codegen/src/openapi/response.rs @@ -12,7 +12,10 @@ use std::{ pub struct Response { /// REQUIRED. A short description of the response. CommonMark syntax MAY be /// used for rich text representation. - pub description: String, + /// Despite being required the description field still may not be included and so + /// must be wrapped in an Option. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<String>, #[serde(default)] #[serde(skip_serializing_if = "HashMap::is_empty")] diff --git a/graph-codegen/src/settings/method_macro_modifier.rs b/graph-codegen/src/settings/method_macro_modifier.rs index 3a3e47e6..742beb2b 100644 --- a/graph-codegen/src/settings/method_macro_modifier.rs +++ b/graph-codegen/src/settings/method_macro_modifier.rs @@ -1097,6 +1097,33 @@ pub fn get_method_macro_modifiers(resource_identity: ResourceIdentity) -> Vec<Me "staff_members", "/staff_members/$count", GeneratedMacroType::FnName("get_staff_members_count") )], + ResourceIdentity::VirtualEventsSessions => vec![ + MethodMacroModifier::fn_name_and_path( + "attendance_records", "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords/$count", + GeneratedMacroType::FnName("get_attendance_records_count") + ), + MethodMacroModifier::fn_name_and_path( + "sessions", "/sessions/$count", + GeneratedMacroType::FnName("get_sessions_count") + ), + MethodMacroModifier::fn_name_and_path( + "attendance_reports", "/sessions/{{RID}}/attendanceReports/$count", + GeneratedMacroType::FnName("get_attendance_reports_count") + )], + ResourceIdentity::VirtualEventsWebinars => vec![ + MethodMacroModifier::fn_name_and_path( + "webinars", "/webinars/$count", + GeneratedMacroType::FnName("get_webinars_count") + ), + MethodMacroModifier::fn_name_and_path( + "registrations", "/webinars/{{RID}}/registrations/$count", + GeneratedMacroType::FnName("get_registrations_count") + )], + ResourceIdentity::VirtualEventsEvents => vec![ + MethodMacroModifier::fn_name_and_path( + "events", "/events/$count", + GeneratedMacroType::FnName("get_events_count") + )], _ => vec![], } } diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 3b976c7b..331a7a45 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1192,6 +1192,7 @@ impl ResourceSettings { vec![ ApiClientLink::Struct("booking_businesses", "BookingBusinessesApiClient"), ApiClientLink::StructId("booking_business", "BookingBusinessesIdApiClient"), + ApiClientLink::Struct("virtual_events", "VirtualEventsApiClient"), ] ) ]) @@ -1214,11 +1215,51 @@ impl ResourceSettings { ApiClientLink::StructId("staff_member", "StaffMembersIdApiClient"), ApiClientLink::Struct("calendar_views", "CalendarViewApiClient"), ApiClientLink::StructId("calendar_view", "CalendarViewIdApiClient"), - ] + ]) + ]) + .build() + .unwrap(), + ResourceIdentity::VirtualEvents => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::solutions::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("VirtualEventsApiClient"), + vec![ + ApiClientLink::Struct("events", "VirtualEventsEventsApiClient"), + ApiClientLink::Struct("webinars", "VirtualEventsWebinarsApiClient"), + ApiClientLink::StructId("event", "VirtualEventsEventsIdApiClient"), + ApiClientLink::StructId("webinar", "VirtualEventsWebinarsIdApiClient"), + ] + ) + ]) + .build() + .unwrap(), + ResourceIdentity::VirtualEventsEvents => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::solutions::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("VirtualEventsEventsIdApiClient"), + vec![ + ApiClientLink::Struct("sessions", "VirtualEventsSessionsApiClient"), + ApiClientLink::StructId("session", "VirtualEventsSessionsIdApiClient"), + ] ) ]) .build() .unwrap(), + ResourceIdentity::VirtualEventsWebinars => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::solutions::*"]) + .api_client_links(vec![ + ApiClientLinkSettings(Some("VirtualEventsWebinarsIdApiClient"), + vec![ + ApiClientLink::Struct("sessions", "VirtualEventsSessionsApiClient"), + ApiClientLink::StructId("session", "VirtualEventsSessionsIdApiClient"), + ] + ) + ]) + .build() + .unwrap(), + ResourceIdentity::VirtualEventsSessions => ResourceSettings::builder(path_name, ri) + .build() + .unwrap(), _ => ResourceSettings::default(path_name, ri), } } @@ -2701,9 +2742,28 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf get_write_configuration(ResourceIdentity::CustomQuestions), get_write_configuration(ResourceIdentity::Customers), get_write_configuration(ResourceIdentity::StaffMembers), + get_write_configuration(ResourceIdentity::VirtualEvents), + get_write_configuration(ResourceIdentity::VirtualEventsEvents), + get_write_configuration(ResourceIdentity::VirtualEventsWebinars), + get_write_configuration(ResourceIdentity::VirtualEventsSessions), ]) .build() .unwrap(), + ResourceIdentity::VirtualEvents => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions") + .filter_path(vec!["sessions", "webinars", "events"]) + .build().unwrap(), + ResourceIdentity::VirtualEventsEvents => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/virtualEvents") + .filter_path(vec!["sessions", "webinars"]) + .build().unwrap(), + ResourceIdentity::VirtualEventsWebinars => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/virtualEvents") + .filter_path(vec!["sessions", "events"]) + .build().unwrap(), + ResourceIdentity::VirtualEventsSessions => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) + .trim_path_start("/solutions/virtualEvents/events/{virtualEvent-id}") + .build().unwrap(), ResourceIdentity::BookingBusinesses => WriteConfiguration::second_level_builder(ResourceIdentity::Solutions, resource_identity) .trim_path_start("/solutions") .filter_path(vec!["appointments", "calendarView", "customQuestions", "customers", "services", "staffMembers"]) diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 9bc99bd2..68802fdb 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -247,6 +247,10 @@ pub enum ResourceIdentity { UsersAttachments, UsersManagedDevices, UsersMessages, + VirtualEvents, + VirtualEventsEvents, + VirtualEventsSessions, + VirtualEventsWebinars, VppTokens, WindowsAutopilotDeviceIdentities, WindowsInformationProtectionPolicies, @@ -379,6 +383,10 @@ impl ToString for ResourceIdentity { ResourceIdentity::TermStoreSetsParentGroup => "parentGroup".into(), ResourceIdentity::TermStoreSetsRelations => "relations".into(), ResourceIdentity::TermStoreSetsTerms => "terms".into(), + ResourceIdentity::VirtualEvents => "virtualEvents".into(), + ResourceIdentity::VirtualEventsEvents => "events".into(), + ResourceIdentity::VirtualEventsSessions => "sessions".into(), + ResourceIdentity::VirtualEventsWebinars => "webinars".into(), ResourceIdentity::WorkbookFunctions => "functions".into(), ResourceIdentity::WorkbookTables => "tables".into(), ResourceIdentity::WorkbookTablesColumns => "columns".into(), diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/events/request.rs b/src/events/request.rs new file mode 100644 index 00000000..8c1b9b6a --- /dev/null +++ b/src/events/request.rs @@ -0,0 +1,45 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +api_client!(VirtualEventsEventsApiClient, VirtualEventsEventsIdApiClient, ResourceIdentity::VirtualEventsEvents); + +impl VirtualEventsEventsApiClient { + post!( + doc: "Create new navigation property to events for solutions", + name: create_events, + path: "/events", + body: true + ); + get!( + doc: "Get events from solutions", + name: list_events, + path: "/events" + ); + get!( + doc: "Get the number of the resource", + name: events, + path: "/events/$count" + ); +} + +impl VirtualEventsEventsIdApiClient {api_client_link_id!(sessions, VirtualEventsSessionsIdApiClient); + + delete!( + doc: "Delete navigation property events for solutions", + name: delete_events, + path: "/events/{{RID}}" + ); + get!( + doc: "Get events from solutions", + name: get_events, + path: "/events/{{RID}}" + ); + patch!( + doc: "Update the navigation property events in solutions", + name: update_events, + path: "/events/{{RID}}", + body: true + ); +} diff --git a/src/solutions/booking_businesses/request.rs b/src/solutions/booking_businesses/request.rs index c9b49854..07b9f8a6 100644 --- a/src/solutions/booking_businesses/request.rs +++ b/src/solutions/booking_businesses/request.rs @@ -30,18 +30,18 @@ impl BookingBusinessesApiClient { } impl BookingBusinessesIdApiClient { - api_client_link_id!(service, ServicesIdApiClient); + api_client_link!(services, ServicesApiClient); api_client_link!(customers, CustomersApiClient); - api_client_link_id!(staff_member, StaffMembersIdApiClient); - api_client_link!(staff_members, StaffMembersApiClient); api_client_link_id!(appointment, AppointmentsIdApiClient); - api_client_link_id!(customer, CustomersIdApiClient); - api_client_link!(custom_questions, CustomQuestionsApiClient); - api_client_link!(appointments, AppointmentsApiClient); + api_client_link_id!(custom_question, CustomQuestionsIdApiClient); + api_client_link!(staff_members, StaffMembersApiClient); api_client_link!(calendar_views, CalendarViewApiClient); - api_client_link!(services, ServicesApiClient); api_client_link_id!(calendar_view, CalendarViewIdApiClient); - api_client_link_id!(custom_question, CustomQuestionsIdApiClient); + api_client_link_id!(staff_member, StaffMembersIdApiClient); + api_client_link!(appointments, AppointmentsApiClient); + api_client_link_id!(service, ServicesIdApiClient); + api_client_link!(custom_questions, CustomQuestionsApiClient); + api_client_link_id!(customer, CustomersIdApiClient); delete!( doc: "Delete bookingBusiness", diff --git a/src/solutions/mod.rs b/src/solutions/mod.rs index b4a42f77..35876f36 100644 --- a/src/solutions/mod.rs +++ b/src/solutions/mod.rs @@ -5,6 +5,10 @@ mod customers; mod request; mod services; mod staff_members; +mod virtual_events; +mod virtual_events_events; +mod virtual_events_sessions; +mod virtual_events_webinars; pub use appointments::*; pub use booking_businesses::*; @@ -13,3 +17,7 @@ pub use customers::*; pub use request::*; pub use services::*; pub use staff_members::*; +pub use virtual_events::*; +pub use virtual_events_events::*; +pub use virtual_events_sessions::*; +pub use virtual_events_webinars::*; diff --git a/src/solutions/request.rs b/src/solutions/request.rs index 2e4a593c..5c0e1572 100644 --- a/src/solutions/request.rs +++ b/src/solutions/request.rs @@ -7,6 +7,7 @@ api_client!(SolutionsApiClient, ResourceIdentity::Solutions); impl SolutionsApiClient { api_client_link!(booking_businesses, BookingBusinessesApiClient); + api_client_link!(virtual_events, VirtualEventsApiClient); api_client_link_id!(booking_business, BookingBusinessesIdApiClient); get!( diff --git a/src/solutions/virtual_events/mod.rs b/src/solutions/virtual_events/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/virtual_events/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/virtual_events/request.rs b/src/solutions/virtual_events/request.rs new file mode 100644 index 00000000..d4d54b4b --- /dev/null +++ b/src/solutions/virtual_events/request.rs @@ -0,0 +1,30 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +api_client!(VirtualEventsApiClient, ResourceIdentity::VirtualEvents); + +impl VirtualEventsApiClient { + api_client_link_id!(webinar, VirtualEventsWebinarsIdApiClient); + api_client_link_id!(event, VirtualEventsEventsIdApiClient); + api_client_link!(events, VirtualEventsEventsApiClient); + api_client_link!(webinars, VirtualEventsWebinarsApiClient); + + delete!( + doc: "Delete navigation property virtualEvents for solutions", + name: delete_virtual_events, + path: "/virtualEvents" + ); + get!( + doc: "Get virtualEvents from solutions", + name: get_virtual_events, + path: "/virtualEvents" + ); + patch!( + doc: "Update the navigation property virtualEvents in solutions", + name: update_virtual_events, + path: "/virtualEvents", + body: true + ); +} diff --git a/src/solutions/virtual_events_events/mod.rs b/src/solutions/virtual_events_events/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/virtual_events_events/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/virtual_events_events/request.rs b/src/solutions/virtual_events_events/request.rs new file mode 100644 index 00000000..f39aaf69 --- /dev/null +++ b/src/solutions/virtual_events_events/request.rs @@ -0,0 +1,51 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +api_client!( + VirtualEventsEventsApiClient, + VirtualEventsEventsIdApiClient, + ResourceIdentity::VirtualEventsEvents +); + +impl VirtualEventsEventsApiClient { + post!( + doc: "Create new navigation property to events for solutions", + name: create_events, + path: "/events", + body: true + ); + get!( + doc: "Get events from solutions", + name: list_events, + path: "/events" + ); + get!( + doc: "Get the number of the resource", + name: get_events_count, + path: "/events/$count" + ); +} + +impl VirtualEventsEventsIdApiClient { + api_client_link!(sessions, VirtualEventsSessionsApiClient); + api_client_link_id!(session, VirtualEventsSessionsIdApiClient); + + delete!( + doc: "Delete navigation property events for solutions", + name: delete_events, + path: "/events/{{RID}}" + ); + get!( + doc: "Get events from solutions", + name: get_events, + path: "/events/{{RID}}" + ); + patch!( + doc: "Update the navigation property events in solutions", + name: update_events, + path: "/events/{{RID}}", + body: true + ); +} diff --git a/src/solutions/virtual_events_sessions/mod.rs b/src/solutions/virtual_events_sessions/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/virtual_events_sessions/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/virtual_events_sessions/request.rs b/src/solutions/virtual_events_sessions/request.rs new file mode 100644 index 00000000..6a541707 --- /dev/null +++ b/src/solutions/virtual_events_sessions/request.rs @@ -0,0 +1,120 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +api_client!( + VirtualEventsSessionsApiClient, + VirtualEventsSessionsIdApiClient, + ResourceIdentity::VirtualEventsSessions +); + +impl VirtualEventsSessionsApiClient { + post!( + doc: "Create new navigation property to sessions for solutions", + name: create_sessions, + path: "/sessions", + body: true + ); + get!( + doc: "Get sessions from solutions", + name: list_sessions, + path: "/sessions" + ); + get!( + doc: "Get the number of the resource", + name: get_sessions_count, + path: "/sessions/$count" + ); +} + +impl VirtualEventsSessionsIdApiClient { + delete!( + doc: "Delete navigation property sessions for solutions", + name: delete_sessions, + path: "/sessions/{{RID}}" + ); + get!( + doc: "Get sessions from solutions", + name: get_sessions, + path: "/sessions/{{RID}}" + ); + patch!( + doc: "Update the navigation property sessions in solutions", + name: update_sessions, + path: "/sessions/{{RID}}", + body: true + ); + post!( + doc: "Create new navigation property to attendanceReports for solutions", + name: create_attendance_reports, + path: "/sessions/{{RID}}/attendanceReports", + body: true + ); + get!( + doc: "Get attendanceReports from solutions", + name: list_attendance_reports, + path: "/sessions/{{RID}}/attendanceReports" + ); + get!( + doc: "Get the number of the resource", + name: get_attendance_reports_count, + path: "/sessions/{{RID}}/attendanceReports/$count" + ); + delete!( + doc: "Delete navigation property attendanceReports for solutions", + name: delete_attendance_reports, + path: "/sessions/{{RID}}/attendanceReports/{{id}}", + params: meeting_attendance_report_id + ); + get!( + doc: "Get attendanceReports from solutions", + name: get_attendance_reports, + path: "/sessions/{{RID}}/attendanceReports/{{id}}", + params: meeting_attendance_report_id + ); + patch!( + doc: "Update the navigation property attendanceReports in solutions", + name: update_attendance_reports, + path: "/sessions/{{RID}}/attendanceReports/{{id}}", + body: true, + params: meeting_attendance_report_id + ); + post!( + doc: "Create new navigation property to attendanceRecords for solutions", + name: create_attendance_records, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords", + body: true, + params: meeting_attendance_report_id + ); + get!( + doc: "Get attendanceRecords from solutions", + name: list_attendance_records, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords", + params: meeting_attendance_report_id + ); + get!( + doc: "Get the number of the resource", + name: get_attendance_records_count, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords/$count", + params: meeting_attendance_report_id + ); + delete!( + doc: "Delete navigation property attendanceRecords for solutions", + name: delete_attendance_records, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords/{{id2}}", + params: meeting_attendance_report_id, attendance_record_id + ); + get!( + doc: "Get attendanceRecords from solutions", + name: get_attendance_records, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords/{{id2}}", + params: meeting_attendance_report_id, attendance_record_id + ); + patch!( + doc: "Update the navigation property attendanceRecords in solutions", + name: update_attendance_records, + path: "/sessions/{{RID}}/attendanceReports/{{id}}/attendanceRecords/{{id2}}", + body: true, + params: meeting_attendance_report_id, attendance_record_id + ); +} diff --git a/src/solutions/virtual_events_webinars/mod.rs b/src/solutions/virtual_events_webinars/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/solutions/virtual_events_webinars/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/solutions/virtual_events_webinars/request.rs b/src/solutions/virtual_events_webinars/request.rs new file mode 100644 index 00000000..a82e4db0 --- /dev/null +++ b/src/solutions/virtual_events_webinars/request.rs @@ -0,0 +1,98 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::solutions::*; + +api_client!( + VirtualEventsWebinarsApiClient, + VirtualEventsWebinarsIdApiClient, + ResourceIdentity::VirtualEventsWebinars +); + +impl VirtualEventsWebinarsApiClient { + post!( + doc: "Create new navigation property to webinars for solutions", + name: create_webinars, + path: "/webinars", + body: true + ); + get!( + doc: "List webinars", + name: list_webinars, + path: "/webinars" + ); + get!( + doc: "Get the number of the resource", + name: get_webinars_count, + path: "/webinars/$count" + ); + get!( + doc: "Invoke function getByUserIdAndRole", + name: get_by_user_id_and_role, + path: "/webinars/getByUserIdAndRole(userId='{{id}}',role='{{id2}}')", + params: user_id, role + ); + get!( + doc: "Invoke function getByUserRole", + name: get_by_user_role, + path: "/webinars/getByUserRole(role='{{id}}')", + params: role + ); +} + +impl VirtualEventsWebinarsIdApiClient { + api_client_link!(sessions, VirtualEventsSessionsApiClient); + api_client_link_id!(session, VirtualEventsSessionsIdApiClient); + + delete!( + doc: "Delete navigation property webinars for solutions", + name: delete_webinars, + path: "/webinars/{{RID}}" + ); + get!( + doc: "Get virtualEventWebinar", + name: get_webinars, + path: "/webinars/{{RID}}" + ); + patch!( + doc: "Update the navigation property webinars in solutions", + name: update_webinars, + path: "/webinars/{{RID}}", + body: true + ); + post!( + doc: "Create new navigation property to registrations for solutions", + name: create_registrations, + path: "/webinars/{{RID}}/registrations", + body: true + ); + get!( + doc: "List virtualEventRegistrations", + name: list_registrations, + path: "/webinars/{{RID}}/registrations" + ); + get!( + doc: "Get the number of the resource", + name: get_registrations_count, + path: "/webinars/{{RID}}/registrations/$count" + ); + delete!( + doc: "Delete navigation property registrations for solutions", + name: delete_registrations, + path: "/webinars/{{RID}}/registrations/{{id}}", + params: virtual_event_registration_id + ); + get!( + doc: "Get virtualEventRegistration", + name: get_registrations, + path: "/webinars/{{RID}}/registrations/{{id}}", + params: virtual_event_registration_id + ); + patch!( + doc: "Update the navigation property registrations in solutions", + name: update_registrations, + path: "/webinars/{{RID}}/registrations/{{id}}", + body: true, + params: virtual_event_registration_id + ); +} From 09199c9f1caf4f87676b237b7fbd4da1a3b959af Mon Sep 17 00:00:00 2001 From: smndtrl <smndtrl@users.noreply.github.com> Date: Fri, 14 Jun 2024 07:05:36 +0000 Subject: [PATCH 115/118] update `reqwest` and `http` --- Cargo.toml | 8 ++++++-- graph-codegen/Cargo.toml | 2 +- graph-core/Cargo.toml | 4 ++-- graph-error/Cargo.toml | 4 ++-- graph-http/Cargo.toml | 4 ++-- graph-oauth/Cargo.toml | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 097564b6..9954aa4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ members = [ [dependencies] handlebars = "2.0.4" # TODO: Update to 4 lazy_static = "1.4.0" -reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" url = "2" @@ -60,10 +60,14 @@ openssl = ["graph-oauth/openssl"] interactive-auth = ["graph-oauth/interactive-auth"] test-util = ["graph-http/test-util"] +[workspace.dependencies] +reqwest = { version = "0.12", default-features = false} +http = { version = "1", default-features = false } + [dev-dependencies] bytes = { version = "1.4.0" } futures = "0.3" -http = "0.2.11" +http = { workspace = true } lazy_static = "1.4" tokio = { version = "1.27.0", features = ["full"] } warp = { version = "0.3.5" } diff --git a/graph-codegen/Cargo.toml b/graph-codegen/Cargo.toml index f533bca6..d7a34865 100644 --- a/graph-codegen/Cargo.toml +++ b/graph-codegen/Cargo.toml @@ -20,7 +20,7 @@ Inflector = "0.11.4" lazy_static = "1.4.0" rayon = "1.5.0" regex = "1" -reqwest = { version = "0.11.16", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1" serde_yaml = "0.9.17" diff --git a/graph-core/Cargo.toml b/graph-core/Cargo.toml index cc6a8d8e..9c943910 100644 --- a/graph-core/Cargo.toml +++ b/graph-core/Cargo.toml @@ -14,11 +14,11 @@ async-trait = "0.1.35" base64 = "0.21.0" dyn-clone = "1.0.14" Inflector = "0.11.4" -http = "0.2.11" +http = { workspace = true } jsonwebtoken = "9.1.0" parking_lot = "0.12.1" percent-encoding = "2" -reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.17" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/graph-error/Cargo.toml b/graph-error/Cargo.toml index 475d4fd4..9d16d60e 100644 --- a/graph-error/Cargo.toml +++ b/graph-error/Cargo.toml @@ -17,9 +17,9 @@ base64 = "0.21.0" futures = "0.3" handlebars = "2.0.2" http-serde = "1" -http = "0.2.11" +http = { workspace = true } jsonwebtoken = "9.1.0" -reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } ring = "0.17" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/graph-http/Cargo.toml b/graph-http/Cargo.toml index b9c1a23b..25e9b84b 100644 --- a/graph-http/Cargo.toml +++ b/graph-http/Cargo.toml @@ -14,9 +14,9 @@ async-trait = "0.1.35" bytes = { version = "1.4.0", features = ["serde"] } futures = "0.3.28" handlebars = "2.0.4" -http = "0.2.11" +http = { workspace = true } percent-encoding = "2" -reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7.1" diff --git a/graph-oauth/Cargo.toml b/graph-oauth/Cargo.toml index 16627a20..3e235bed 100644 --- a/graph-oauth/Cargo.toml +++ b/graph-oauth/Cargo.toml @@ -22,11 +22,11 @@ async-trait = "0.1.35" base64 = "0.21.0" dyn-clone = "1.0.14" hex = "0.4.3" -http = "0.2.11" +http = { workspace = true } jsonwebtoken = "9.1.0" lazy_static = "1.4.0" openssl = { version = "0.10", optional=true } -reqwest = { version = "0.11.22", default-features=false, features = ["json", "gzip", "blocking", "stream"] } +reqwest = { workspace = true, default-features=false, features = ["json", "gzip", "blocking", "stream"] } serde = { version = "1", features = ["derive"] } serde-aux = "4.1.2" serde_json = "1" From e5c718ea6cfd8849f07f3f019aa035e20c878108 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:09:43 -0400 Subject: [PATCH 116/118] Fix comment tests related to 2.0 release --- graph-oauth/src/identity/authority.rs | 2 +- graph-oauth/src/identity/token.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/graph-oauth/src/identity/authority.rs b/graph-oauth/src/identity/authority.rs index 591b8928..066eeb1e 100644 --- a/graph-oauth/src/identity/authority.rs +++ b/graph-oauth/src/identity/authority.rs @@ -152,7 +152,7 @@ pub enum AadAuthorityAudience { /// /// # Using Tenant Id /// ```rust - /// # use graph_oauth::oauth::AadAuthorityAudience; + /// use graph_oauth::AadAuthorityAudience; /// let authority_audience = AadAuthorityAudience::AzureAdMyOrg("tenant_id".into()); /// ``` AzureAdMyOrg(String), diff --git a/graph-oauth/src/identity/token.rs b/graph-oauth/src/identity/token.rs index 8e8deb3c..59aadc05 100644 --- a/graph-oauth/src/identity/token.rs +++ b/graph-oauth/src/identity/token.rs @@ -259,10 +259,10 @@ impl Token { /// /// # Example /// ``` - /// # use graph_oauth::oauth::{Token, IdToken}; + /// # use graph_oauth::{Token, IdToken}; /// /// let mut access_token = Token::default(); - /// access_token.with_id_token(IdToken::new("id_token", "code", "state", "session_state")); + /// access_token.with_id_token(IdToken::new("id_token", Some("code"), Some("state"), Some("session_state"))); /// ``` pub fn with_id_token(&mut self, id_token: IdToken) { self.id_token = Some(id_token); From afda1dcfbf5d491e56c6753152a1ce7e1c97fb44 Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Sat, 15 Jun 2024 21:07:50 -0400 Subject: [PATCH 117/118] Update README.md --- README.md | 39 ++++++++------------------------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index cfad213d..4bbe6f4d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ### Available on [crates.io](https://crates.io/crates/graph-rs-sdk/2.0.0) - v2.0.0 - Latest Stable Version -#### Features: +#### Feature Overview: [Microsoft Graph V1 and Beta API Client](#graph-client) - Wide support for Graph APIs @@ -21,6 +21,8 @@ - Interactive WebView Auth (feature = `interactive-auth`) - X509 Certificate (feature = `openssl`) and Proof Key Code Exchange (PKCE) Support +And much more. See [Features](#features) for a more comprehensive list of features. + ```toml graph-rs-sdk = "2.0.0" tokio = { version = "1.25.0", features = ["full"] } @@ -46,19 +48,6 @@ use futures::StreamExt; use graph_rs_sdk::*; ``` -Contributing and Wiki: -- [Contributions](https://github.com/sreeise/graph-rs-sdk/wiki/Contributing) -- [Wiki](https://github.com/sreeise/graph-rs-sdk/wiki) - -### Feature requests or Bug reports. - -For bug reports please file an issue on GitHub and a response or fix will be given as soon as possible. - -The [Discussions](https://github.com/sreeise/graph-rs-sdk/discussions) tab on [GitHub](https://github.com/sreeise/graph-rs-sdk/discussions) -is enabled so feel free to stop by there with any questions or feature requests as well. For bugs, please file -an issue first. Features can be requested through issues or discussions. Either way works. -Other than that feel free to ask questions, provide tips to others, and talk about the project in general. - ## Features ### Graph Client @@ -72,6 +61,7 @@ Other than that feel free to ask questions, provide tips to others, and talk abo * [Streaming](#streaming) * [Channels](#channels) * [API Usage](#api-usage) + * [Batch Requests](#batch-requests) * [Id vs Non-Id methods](#id-vs-non-id-methods-such-as-useruser-id-vs-users) * [Contributing](#contributing) * [Wiki](#wiki) @@ -176,18 +166,6 @@ fn main() -> GraphResult<()> { } ``` -## Cargo Feature Flags - -- `native-tls`: Use the `native-tls` TLS backend (OpenSSL on *nix, SChannel on Windows, Secure Transport on macOS). -- `rustls-tls`: Use the `rustls-tls` TLS backend (cross-platform backend, only supports TLS 1.2 and 1.3). -- `brotli`: Enables reqwest feature brotli. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -- `deflate`: Enables reqwest feature deflate. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -- `trust-dns`: Enables reqwest feature trust-dns. For more info see the [reqwest](https://crates.io/crates/reqwest) crate. -- `test-util`: Enables testing features. Adds the ability to set a custom endpoint for mocking frameworks using the `use_test_endpoint` method on the `GraphClient`. - - Also allow http (disable https only) - -Default features: `default=["native-tls"]` - #### The send method The send() method is the main method for sending a request and returns a `Result<rewest::Response, GraphFailure>`. See the [reqwest](https://crates.io/crates/reqwest) crate for information on the Response type. @@ -893,8 +871,7 @@ async fn create_message() -> GraphResult<()> { ### Batch Requests -Batch requests use a mpsc::channel and return the receiver -for responses. +Call multiple Graph APIs in a single request. ```rust use graph_rs_sdk::*; @@ -1030,10 +1007,10 @@ Support for: - [Identity Platform Auth Examples](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth) - [Auth Code Grant](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/auth_code_grant) - - [OpenId]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/openid)) - - [Client Credentials]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/client_credentials)) + - [OpenId](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/openid) + - [Client Credentials](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth/client_credentials) - [Url Builders For Flows Using Sign In To Get Authorization Code - Building Sign In Url](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/authorization_sign_in) -- [Interactive Auth Examples (feature = `interactive-auth`)]((https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth)) +- [Interactive Auth Examples (feature = `interactive-auth`)](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/identity_platform_auth) - [Certificate Auth (feature = `openssl`)](https://github.com/sreeise/graph-rs-sdk/tree/master/examples/certificate_auth) There are two main types for building your chosen OAuth or OpenId Connect Flow. From 4902a081788bb9688260ddc7d6442a59a644107b Mon Sep 17 00:00:00 2001 From: Sean Reeise <35837533+sreeise@users.noreply.github.com> Date: Thu, 11 Jul 2024 07:10:03 -0400 Subject: [PATCH 118/118] Add devices api. Closes #482 --- .../src/settings/method_macro_modifier.rs | 42 +++++ .../src/settings/resource_settings.rs | 42 +++++ graph-core/src/resource/resource_identity.rs | 4 + src/client/graph.rs | 3 + src/devices/devices_registered_owners/mod.rs | 3 + .../devices_registered_owners/request.rs | 106 +++++++++++ src/devices/devices_registered_users/mod.rs | 3 + .../devices_registered_users/request.rs | 106 +++++++++++ src/devices/mod.rs | 7 + src/devices/request.rs | 167 ++++++++++++++++++ src/lib.rs | 1 + src/users/member_of/request.rs | 20 ++- src/users/transitive_member_of/request.rs | 22 ++- 13 files changed, 521 insertions(+), 5 deletions(-) create mode 100644 src/devices/devices_registered_owners/mod.rs create mode 100644 src/devices/devices_registered_owners/request.rs create mode 100644 src/devices/devices_registered_users/mod.rs create mode 100644 src/devices/devices_registered_users/request.rs create mode 100644 src/devices/mod.rs create mode 100644 src/devices/request.rs diff --git a/graph-codegen/src/settings/method_macro_modifier.rs b/graph-codegen/src/settings/method_macro_modifier.rs index 742beb2b..ba9b5c4c 100644 --- a/graph-codegen/src/settings/method_macro_modifier.rs +++ b/graph-codegen/src/settings/method_macro_modifier.rs @@ -1124,6 +1124,48 @@ pub fn get_method_macro_modifiers(resource_identity: ResourceIdentity) -> Vec<Me "events", "/events/$count", GeneratedMacroType::FnName("get_events_count") )], + ResourceIdentity::Devices => vec![ + MethodMacroModifier::fn_name_and_path("devices_get_count_3489", "/devices/$count", + GeneratedMacroType::FnName("get_devices_count") + ), + MethodMacroModifier::fn_name_and_path("extensions", "/devices/{{RID}}/extensions/$count", + GeneratedMacroType::FnName("get_extensions_count") + ) + ], + ResourceIdentity::DevicesRegisteredOwners => vec![ + MethodMacroModifier::fn_name_and_path("registered_owners", "/registeredOwners/$count", + GeneratedMacroType::FnName("get_devices_registered_owners_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredOwners/graph.user/$count", + GeneratedMacroType::FnName("get_user_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredOwners/graph.servicePrincipal/$count", + GeneratedMacroType::FnName("get_service_principal_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredOwners/graph.endpoint/$count", + GeneratedMacroType::FnName("get_endpoint_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredOwners/graph.appRoleAssignment/$count", + GeneratedMacroType::FnName("get_app_role_assignment_count") + ) + ], + ResourceIdentity::DevicesRegisteredUsers => vec![ + MethodMacroModifier::fn_name_and_path("registered_users", "/registeredUsers/$count", + GeneratedMacroType::FnName("get_devices_registered_owners_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredUsers/graph.user/$count", + GeneratedMacroType::FnName("get_user_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredUsers/graph.servicePrincipal/$count", + GeneratedMacroType::FnName("get_service_principal_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredUsers/graph.endpoint/$count", + GeneratedMacroType::FnName("get_endpoint_count") + ), + MethodMacroModifier::fn_name_and_path("get_count", "/registeredUsers/graph.appRoleAssignment/$count", + GeneratedMacroType::FnName("get_app_role_assignment_count") + ) + ], _ => vec![], } } diff --git a/graph-codegen/src/settings/resource_settings.rs b/graph-codegen/src/settings/resource_settings.rs index 331a7a45..5122b87b 100644 --- a/graph-codegen/src/settings/resource_settings.rs +++ b/graph-codegen/src/settings/resource_settings.rs @@ -1260,7 +1260,23 @@ impl ResourceSettings { ResourceIdentity::VirtualEventsSessions => ResourceSettings::builder(path_name, ri) .build() .unwrap(), + ResourceIdentity::Devices => ResourceSettings::builder(path_name, ri) + .imports(vec!["crate::users::TransitiveMemberOfApiClient", "crate::users::MemberOfApiClient", "crate::users::TransitiveMemberOfIdApiClient", "crate::users::MemberOfIdApiClient", "crate::devices::*"]) + .api_client_links(vec![ApiClientLinkSettings(Some("DevicesIdApiClient"), vec![ + ApiClientLink::StructId("registered_user", "DevicesRegisteredUsersIdApiClient"), + ApiClientLink::Struct("registered_users", "DevicesRegisteredUsersApiClient"), + ApiClientLink::StructId("registered_owner", "DevicesRegisteredOwnersIdApiClient"), + ApiClientLink::Struct("registered_owners", "DevicesRegisteredOwnersApiClient"), + ApiClientLink::StructId("transitive_member_of", "TransitiveMemberOfIdApiClient"), + ApiClientLink::StructId("member_of", "MemberOfIdApiClient"), + ApiClientLink::Struct("transitive_members_of", "TransitiveMemberOfApiClient"), + ApiClientLink::Struct("members_of", "MemberOfApiClient"), + ] + ) + ]) + .build().unwrap(), _ => ResourceSettings::default(path_name, ri), + } } } @@ -2099,6 +2115,32 @@ pub fn get_write_configuration(resource_identity: ResourceIdentity) -> WriteConf ResourceIdentity::DomainDnsRecords => WriteConfiguration::from(resource_identity), + ResourceIdentity::Devices => WriteConfiguration::builder(resource_identity) + .filter_path(vec!["registeredOwners", "registeredUsers", "memberOf", "transitiveMemberOf"]) + .imports(vec!["crate::users::TransitiveMemberOfApiClient", "crate::users::MemberOfApiClient", "crate::users::TransitiveMemberOfIdApiClient", "crate::users::MemberOfIdApiClient", "crate::devices::*"]) + .children(vec![ + get_write_configuration(ResourceIdentity::DevicesRegisteredOwners), + get_write_configuration(ResourceIdentity::DevicesRegisteredUsers), + ]) + .api_client_links(vec![ApiClientLinkSettings(Some("DevicesIdApiClient"), vec![ + ApiClientLink::StructId("registered_user", "DevicesRegisteredUsersIdApiClient"), + ApiClientLink::Struct("registered_users", "DevicesRegisteredUsersApiClient"), + ApiClientLink::StructId("registered_owner", "DevicesRegisteredOwnersIdApiClient"), + ApiClientLink::Struct("registered_owners", "DevicesRegisteredOwnersApiClient"), + ApiClientLink::StructId("transitive_member_of", "TransitiveMemberOfIdApiClient"), + ApiClientLink::StructId("member_of", "MemberOfIdApiClient"), + ApiClientLink::Struct("transitive_members_of", "TransitiveMemberOfApiClient"), + ApiClientLink::Struct("members_of", "MemberOfApiClient"), + ] + ) + ]) + .build() + .unwrap(), + + ResourceIdentity::DevicesRegisteredUsers | ResourceIdentity::DevicesRegisteredOwners => WriteConfiguration::second_level_builder(ResourceIdentity::Devices, resource_identity) + .trim_path_start("/devices/{device-id}") + .build() + .unwrap(), // Identity Governance ResourceIdentity::EntitlementManagement => WriteConfiguration::second_level_builder(ResourceIdentity::Directory, resource_identity) diff --git a/graph-core/src/resource/resource_identity.rs b/graph-core/src/resource/resource_identity.rs index 68802fdb..0c7e6d7e 100644 --- a/graph-core/src/resource/resource_identity.rs +++ b/graph-core/src/resource/resource_identity.rs @@ -96,6 +96,8 @@ pub enum ResourceIdentity { DeviceManagementManagedDevices, DeviceManagementReports, Devices, + DevicesRegisteredOwners, + DevicesRegisteredUsers, Directory, DirectoryMembers, DirectoryObjects, @@ -401,6 +403,8 @@ impl ToString for ResourceIdentity { ResourceIdentity::WorksheetsChartsAxesCategoryAxis => "categoryAxis".into(), ResourceIdentity::WorksheetsChartsAxesSeriesAxis => "seriesAxis".into(), ResourceIdentity::WorksheetsChartsAxesValueAxis => "valueAxis".into(), + ResourceIdentity::DevicesRegisteredOwners => "registeredOwners".into(), + ResourceIdentity::DevicesRegisteredUsers => "registeredUsers".into(), ResourceIdentity::Custom => "".into(), _ => self.as_ref().to_camel_case(), diff --git a/src/client/graph.rs b/src/client/graph.rs index 05bd840b..3981ec14 100644 --- a/src/client/graph.rs +++ b/src/client/graph.rs @@ -23,6 +23,7 @@ use crate::data_policy_operations::DataPolicyOperationsApiClient; use crate::default_drive::DefaultDriveApiClient; use crate::device_app_management::DeviceAppManagementApiClient; use crate::device_management::DeviceManagementApiClient; +use crate::devices::{DevicesApiClient, DevicesIdApiClient}; use crate::directory::DirectoryApiClient; use crate::directory_objects::{DirectoryObjectsApiClient, DirectoryObjectsIdApiClient}; use crate::directory_role_templates::{ @@ -364,6 +365,8 @@ impl GraphClient { api_client_impl!(device_management, DeviceManagementApiClient); + api_client_impl!(devices, DevicesApiClient, device, DevicesIdApiClient); + api_client_impl!(directory, DirectoryApiClient); api_client_impl!( diff --git a/src/devices/devices_registered_owners/mod.rs b/src/devices/devices_registered_owners/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/devices/devices_registered_owners/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/devices/devices_registered_owners/request.rs b/src/devices/devices_registered_owners/request.rs new file mode 100644 index 00000000..6dbf3bdd --- /dev/null +++ b/src/devices/devices_registered_owners/request.rs @@ -0,0 +1,106 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +api_client!( + DevicesRegisteredOwnersApiClient, + DevicesRegisteredOwnersIdApiClient, + ResourceIdentity::DevicesRegisteredOwners +); + +impl DevicesRegisteredOwnersApiClient { + get!( + doc: "List registeredOwners", + name: list_registered_owners, + path: "/registeredOwners" + ); + get!( + doc: "Get the number of the resource", + name: get_devices_registered_owners_count, + path: "/registeredOwners/$count" + ); + post!( + doc: "Create registeredOwner", + name: create_ref_registered_owners, + path: "/registeredOwners/$ref", + body: true + ); + delete!( + doc: "Delete registeredOwner", + name: delete_ref_registered_owners, + path: "/registeredOwners/$ref" + ); + get!( + doc: "List registeredOwners", + name: list_ref_registered_owners, + path: "/registeredOwners/$ref" + ); + get!( + doc: "Get the items of type microsoft.graph.appRoleAssignment in the microsoft.graph.directoryObject collection", + name: as_app_role_assignment, + path: "/registeredOwners/graph.appRoleAssignment" + ); + get!( + doc: "Get the number of the resource", + name: get_app_role_assignment_count, + path: "/registeredOwners/graph.appRoleAssignment/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.endpoint in the microsoft.graph.directoryObject collection", + name: as_endpoint, + path: "/registeredOwners/graph.endpoint" + ); + get!( + doc: "Get the number of the resource", + name: get_endpoint_count, + path: "/registeredOwners/graph.endpoint/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.servicePrincipal in the microsoft.graph.directoryObject collection", + name: as_service_principal, + path: "/registeredOwners/graph.servicePrincipal" + ); + get!( + doc: "Get the number of the resource", + name: get_service_principal_count, + path: "/registeredOwners/graph.servicePrincipal/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.user in the microsoft.graph.directoryObject collection", + name: as_user, + path: "/registeredOwners/graph.user" + ); + get!( + doc: "Get the number of the resource", + name: get_user_count, + path: "/registeredOwners/graph.user/$count" + ); +} + +impl DevicesRegisteredOwnersIdApiClient { + delete!( + doc: "Delete registeredOwner", + name: delete_ref_directory_object, + path: "/registeredOwners/{{RID}}/$ref" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.appRoleAssignment", + name: as_app_role_assignment, + path: "/registeredOwners/{{RID}}/graph.appRoleAssignment" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.endpoint", + name: as_endpoint, + path: "/registeredOwners/{{RID}}/graph.endpoint" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.servicePrincipal", + name: as_service_principal, + path: "/registeredOwners/{{RID}}/graph.servicePrincipal" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.user", + name: as_user, + path: "/registeredOwners/{{RID}}/graph.user" + ); +} diff --git a/src/devices/devices_registered_users/mod.rs b/src/devices/devices_registered_users/mod.rs new file mode 100644 index 00000000..3edd9a21 --- /dev/null +++ b/src/devices/devices_registered_users/mod.rs @@ -0,0 +1,3 @@ +mod request; + +pub use request::*; diff --git a/src/devices/devices_registered_users/request.rs b/src/devices/devices_registered_users/request.rs new file mode 100644 index 00000000..695bb247 --- /dev/null +++ b/src/devices/devices_registered_users/request.rs @@ -0,0 +1,106 @@ +// GENERATED CODE + +use crate::api_default_imports::*; + +api_client!( + DevicesRegisteredUsersApiClient, + DevicesRegisteredUsersIdApiClient, + ResourceIdentity::DevicesRegisteredUsers +); + +impl DevicesRegisteredUsersApiClient { + get!( + doc: "List registeredUsers", + name: list_registered_users, + path: "/registeredUsers" + ); + get!( + doc: "Get the number of the resource", + name: get_devices_registered_owners_count, + path: "/registeredUsers/$count" + ); + post!( + doc: "Create registeredUser", + name: create_ref_registered_users, + path: "/registeredUsers/$ref", + body: true + ); + delete!( + doc: "Delete registeredUser", + name: delete_ref_registered_users, + path: "/registeredUsers/$ref" + ); + get!( + doc: "List registeredUsers", + name: list_ref_registered_users, + path: "/registeredUsers/$ref" + ); + get!( + doc: "Get the items of type microsoft.graph.appRoleAssignment in the microsoft.graph.directoryObject collection", + name: as_app_role_assignment, + path: "/registeredUsers/graph.appRoleAssignment" + ); + get!( + doc: "Get the number of the resource", + name: get_app_role_assignment_count, + path: "/registeredUsers/graph.appRoleAssignment/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.endpoint in the microsoft.graph.directoryObject collection", + name: as_endpoint, + path: "/registeredUsers/graph.endpoint" + ); + get!( + doc: "Get the number of the resource", + name: get_endpoint_count, + path: "/registeredUsers/graph.endpoint/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.servicePrincipal in the microsoft.graph.directoryObject collection", + name: as_service_principal, + path: "/registeredUsers/graph.servicePrincipal" + ); + get!( + doc: "Get the number of the resource", + name: get_service_principal_count, + path: "/registeredUsers/graph.servicePrincipal/$count" + ); + get!( + doc: "Get the items of type microsoft.graph.user in the microsoft.graph.directoryObject collection", + name: as_user, + path: "/registeredUsers/graph.user" + ); + get!( + doc: "Get the number of the resource", + name: get_user_count, + path: "/registeredUsers/graph.user/$count" + ); +} + +impl DevicesRegisteredUsersIdApiClient { + delete!( + doc: "Delete registeredUser", + name: delete_ref_directory_object, + path: "/registeredUsers/{{RID}}/$ref" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.appRoleAssignment", + name: as_app_role_assignment, + path: "/registeredUsers/{{RID}}/graph.appRoleAssignment" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.endpoint", + name: as_endpoint, + path: "/registeredUsers/{{RID}}/graph.endpoint" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.servicePrincipal", + name: as_service_principal, + path: "/registeredUsers/{{RID}}/graph.servicePrincipal" + ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.user", + name: as_user, + path: "/registeredUsers/{{RID}}/graph.user" + ); +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 00000000..0f02a52f --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,7 @@ +mod devices_registered_owners; +mod devices_registered_users; +mod request; + +pub use devices_registered_owners::*; +pub use devices_registered_users::*; +pub use request::*; diff --git a/src/devices/request.rs b/src/devices/request.rs new file mode 100644 index 00000000..d726cc4a --- /dev/null +++ b/src/devices/request.rs @@ -0,0 +1,167 @@ +// GENERATED CODE + +use crate::api_default_imports::*; +use crate::devices::*; +use crate::users::MemberOfApiClient; +use crate::users::MemberOfIdApiClient; +use crate::users::TransitiveMemberOfApiClient; +use crate::users::TransitiveMemberOfIdApiClient; + +api_client!( + DevicesApiClient, + DevicesIdApiClient, + ResourceIdentity::Devices +); + +impl DevicesApiClient { + post!( + doc: "Create device", + name: create_device, + path: "/devices", + body: true + ); + get!( + doc: "List devices", + name: list_device, + path: "/devices" + ); + delete!( + doc: "Delete device", + name: delete_device_by_device_id, + path: "/devices(deviceId='{{id}}')", + params: device_id + ); + get!( + doc: "Get device", + name: get_device_by_device_id, + path: "/devices(deviceId='{{id}}')", + params: device_id + ); + patch!( + doc: "Update device", + name: update_device_by_device_id, + path: "/devices(deviceId='{{id}}')", + body: true, + params: device_id + ); + get!( + doc: "Get the number of the resource", + name: get_devices_count, + path: "/devices/$count" + ); + get!( + doc: "Invoke function delta", + name: delta, + path: "/devices/delta()" + ); + post!( + doc: "Invoke action getAvailableExtensionProperties", + name: get_available_extension_properties, + path: "/devices/getAvailableExtensionProperties", + body: true + ); + post!( + doc: "Invoke action getByIds", + name: get_by_ids, + path: "/devices/getByIds", + body: true + ); + post!( + doc: "Invoke action validateProperties", + name: validate_properties, + path: "/devices/validateProperties", + body: true + ); +} + +impl DevicesIdApiClient { + api_client_link!(registered_users, DevicesRegisteredUsersApiClient); + api_client_link_id!(registered_user, DevicesRegisteredUsersIdApiClient); + api_client_link!(members_of, MemberOfApiClient); + api_client_link_id!(registered_owner, DevicesRegisteredOwnersIdApiClient); + api_client_link!(transitive_members_of, TransitiveMemberOfApiClient); + api_client_link_id!(member_of, MemberOfIdApiClient); + api_client_link_id!(transitive_member_of, TransitiveMemberOfIdApiClient); + api_client_link!(registered_owners, DevicesRegisteredOwnersApiClient); + + delete!( + doc: "Delete device", + name: delete_device, + path: "/devices/{{RID}}" + ); + get!( + doc: "Get device", + name: get_device, + path: "/devices/{{RID}}" + ); + patch!( + doc: "Update device", + name: update_device, + path: "/devices/{{RID}}", + body: true + ); + post!( + doc: "Invoke action checkMemberGroups", + name: check_member_groups, + path: "/devices/{{RID}}/checkMemberGroups", + body: true + ); + post!( + doc: "Invoke action checkMemberObjects", + name: check_member_objects, + path: "/devices/{{RID}}/checkMemberObjects", + body: true + ); + post!( + doc: "Create new navigation property to extensions for devices", + name: create_extensions, + path: "/devices/{{RID}}/extensions", + body: true + ); + get!( + doc: "Get extensions from devices", + name: list_extensions, + path: "/devices/{{RID}}/extensions" + ); + get!( + doc: "Get the number of the resource", + name: get_extensions_count, + path: "/devices/{{RID}}/extensions/$count" + ); + delete!( + doc: "Delete navigation property extensions for devices", + name: delete_extensions, + path: "/devices/{{RID}}/extensions/{{id}}", + params: extension_id + ); + get!( + doc: "Get extensions from devices", + name: get_extensions, + path: "/devices/{{RID}}/extensions/{{id}}", + params: extension_id + ); + patch!( + doc: "Update the navigation property extensions in devices", + name: update_extensions, + path: "/devices/{{RID}}/extensions/{{id}}", + body: true, + params: extension_id + ); + post!( + doc: "Invoke action getMemberGroups", + name: get_member_groups, + path: "/devices/{{RID}}/getMemberGroups", + body: true + ); + post!( + doc: "Invoke action getMemberObjects", + name: get_member_objects, + path: "/devices/{{RID}}/getMemberObjects", + body: true + ); + post!( + doc: "Invoke action restore", + name: restore, + path: "/devices/{{RID}}/restore" + ); +} diff --git a/src/lib.rs b/src/lib.rs index f7bb47eb..adfa4cde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,6 +193,7 @@ pub mod data_policy_operations; pub mod default_drive; pub mod device_app_management; pub mod device_management; +pub mod devices; pub mod directory; pub mod directory_objects; pub mod directory_role_templates; diff --git a/src/users/member_of/request.rs b/src/users/member_of/request.rs index 85e47fc7..a026d1cc 100644 --- a/src/users/member_of/request.rs +++ b/src/users/member_of/request.rs @@ -21,7 +21,7 @@ impl MemberOfApiClient { ); get!( doc: "Get the items of type microsoft.graph.group in the microsoft.graph.directoryObject collection", - name: graph, + name: as_group, path: "/memberOf/graph.group" ); get!( @@ -29,6 +29,16 @@ impl MemberOfApiClient { name: get_group_count, path: "/memberOf/graph.group/$count" ); + get!( + doc: "Get the items of type microsoft.graph.administrativeUnit in the microsoft.graph.directoryObject collection", + name: as_administrative_unit, + path: "/memberOf/graph.administrativeUnit" + ); + get!( + doc: "Get the number of the resource", + name: get_administrative_unit_count, + path: "/memberOf/graph.administrativeUnit/$count" + ); } impl MemberOfIdApiClient { @@ -39,7 +49,13 @@ impl MemberOfIdApiClient { ); get!( doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.group", - name: get_directory_object_item_as_group_type, + name: as_group, path: "/memberOf/{{RID}}/graph.group" ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.administrativeUnit", + name: as_administrative_unit, + path: "/memberOf/{{id}}/graph.administrativeUnit", + params: directory_object_id + ); } diff --git a/src/users/transitive_member_of/request.rs b/src/users/transitive_member_of/request.rs index 8ef4d26e..19a6700c 100644 --- a/src/users/transitive_member_of/request.rs +++ b/src/users/transitive_member_of/request.rs @@ -21,7 +21,7 @@ impl TransitiveMemberOfApiClient { ); get!( doc: "Get the items of type microsoft.graph.group in the microsoft.graph.directoryObject collection", - name: graph, + name: as_group, path: "/transitiveMemberOf/graph.group" ); get!( @@ -29,17 +29,33 @@ impl TransitiveMemberOfApiClient { name: get_group_count, path: "/transitiveMemberOf/graph.group/$count" ); + get!( + doc: "Get the items of type microsoft.graph.administrativeUnit in the microsoft.graph.directoryObject collection", + name: as_administrative_unit, + path: "/transitiveMemberOf/graph.administrativeUnit" + ); + get!( + doc: "Get the number of the resource", + name: get_administrative_unit_count, + path: "/transitiveMemberOf/graph.administrativeUnit/$count" + ); } impl TransitiveMemberOfIdApiClient { get!( - doc: "Get transitiveMemberOf from users", + doc: "Get transitiveMemberOf for the resource", name: get_transitive_member_of, path: "/transitiveMemberOf/{{RID}}" ); get!( doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.group", - name: get_directory_object_item_as_group_type, + name: as_group, path: "/transitiveMemberOf/{{RID}}/graph.group" ); + get!( + doc: "Get the item of type microsoft.graph.directoryObject as microsoft.graph.administrativeUnit", + name: as_administrative_unit, + path: "/devices/{{RID}}/transitiveMemberOf/{{id}}/graph.administrativeUnit", + params: directory_object_id + ); }