diff --git a/src/jwt_profile.rs b/src/jwt_profile.rs new file mode 100644 index 0000000..aa1643f --- /dev/null +++ b/src/jwt_profile.rs @@ -0,0 +1,514 @@ +use std::ops::Deref; +use std::{marker::PhantomData, time::Duration}; + +use crate::{ + jwt::{JsonWebToken, JsonWebTokenJsonPayloadSerde}, + types::helpers::{serde_utc_seconds, serde_utc_seconds_opt}, + AdditionalClaims, Audience, AuthDisplay, AuthPrompt, GenderClaim, JsonWebKey, JsonWebKeyType, + JsonWebKeyUse, JsonWebTokenError, JweContentEncryptionAlgorithm, JwsSigningAlgorithm, + PrivateSigningKey, TokenResponse, +}; +use chrono::{DateTime, Utc}; +use oauth2::{ + ClientCredentialsTokenRequest, ErrorResponse, RevocableToken, TokenIntrospectionResponse, + TokenType, +}; +use rand::{thread_rng, Rng}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; +/// +/// Additional claims beyond the set of Standard Claims defined by OpenID Connect Core. +/// +pub trait AdditionalClientAuthTokenClaims: Debug + DeserializeOwned + Serialize + 'static {} + +/// +/// No additional claims. +/// +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +// In order to support serde flatten, this must be an empty struct rather than an empty +// tuple struct. +pub struct EmptyAdditionalClientAuthTokenClaims {} +impl AdditionalClientAuthTokenClaims for EmptyAdditionalClientAuthTokenClaims {} + +/// FIXME: documentation +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ClientAuthTokenClaims { + #[serde(rename = "iss")] + issuer: String, + + #[serde(rename = "sub")] + subject: String, + + #[serde(rename = "aud")] + audience: Audience, + + #[serde(rename = "exp", with = "serde_utc_seconds")] + expiration: DateTime, + + #[serde( + rename = "nbf", + default, + with = "serde_utc_seconds_opt", + skip_serializing_if = "Option::is_none" + )] + not_before: Option>, + + #[serde( + rename = "iat", + default, + with = "serde_utc_seconds_opt", + skip_serializing_if = "Option::is_none" + )] + issued_at: Option>, + + #[serde(rename = "jti", default, skip_serializing_if = "Option::is_none")] + jwt_id: Option, + + #[serde(bound = "AC: AdditionalClientAuthTokenClaims")] + #[serde(flatten)] + additional_claims: AC, +} + +new_type![ + /// + /// Set of authentication methods or procedures that are considered to be equivalent to each + /// other in a particular context. + /// + #[derive(Deserialize, Serialize)] + ClientAuthTokenId(String) +]; +impl ClientAuthTokenId { + /// + /// Generate a new random, base64-encoded 128-bit CSRF token. + /// + pub fn new_random() -> Self { + ClientAuthTokenId::new_random_len(16) + } + /// + /// Generate a new random, base64-encoded ClientAuthTokenId of the specified length. + /// + /// # Arguments + /// + /// * `num_bytes` - Number of random bytes to generate, prior to base64-encoding. + /// + pub fn new_random_len(num_bytes: u32) -> Self { + let random_bytes: Vec = (0..num_bytes).map(|_| thread_rng().gen::()).collect(); + ClientAuthTokenId::new(base64::encode_config(random_bytes, base64::URL_SAFE_NO_PAD)) + } +} + +impl + crate::Client +where + // AC: AdditionalClientAuthTokenClaims, + AC: AdditionalClaims, + // AC: AdditionalClaims, + AD: AuthDisplay, + GC: GenderClaim, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + P: AuthPrompt, + TE: ErrorResponse + 'static, + TR: TokenResponse, + TT: TokenType + 'static, + TIR: TokenIntrospectionResponse, + RT: RevocableToken, + TRE: ErrorResponse + 'static, +{ + /// FIXME: documentation + pub fn client_auth_token_builder( + &self, + signing_key: S, + signing_algo: JS, + duration: Duration, + jwt_id_method: RF, + additional_claims: ATC, + ) -> ClientAuthTokenBuilder + where + RF: FnOnce() -> ClientAuthTokenId, + ATC: AdditionalClientAuthTokenClaims, + S: PrivateSigningKey, + { + ClientAuthTokenBuilder::new( + self.client_id.to_string(), + self.client_id.to_string(), + Audience::new(self.oauth2_client.token_url().unwrap().to_string()), + signing_key, + signing_algo, + duration, + jwt_id_method, + additional_claims, + ) + } + + /// FIXME: documentation + pub fn exchange_client_credential_with_auth_token( + &self, + token: ClientAuthToken, + ) -> Result, JsonWebTokenError> + where + ATC: AdditionalClientAuthTokenClaims, + { + let ccrt = self + .exchange_client_credentials() + .add_extra_param( + "client_assertion_type", + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + ) + .add_extra_param("client_assertion", token.to_string()); + + Ok(ccrt) + } +} + +/// FIXME: documentation +pub struct ClientAuthTokenBuilder< + AC: AdditionalClientAuthTokenClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + RF: FnOnce() -> ClientAuthTokenId, + SK: PrivateSigningKey, +> { + issuer: String, + subject: String, + audience: Audience, + duration: Duration, + jwt_id_method: RF, + signing_key: SK, + signing_algo: JS, + additional_claims: AC, + include_nbf: bool, + include_iat: bool, + include_jti: bool, + _phantom_jt: PhantomData<(AC, JE, JS, RF, JT, JU, K, JS)>, +} + +impl ClientAuthTokenBuilder +where + AC: AdditionalClientAuthTokenClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, + JU: JsonWebKeyUse, + K: JsonWebKey, + RF: FnOnce() -> ClientAuthTokenId, + SK: PrivateSigningKey, +{ + /// FIXME: documentation + #[allow(clippy::too_many_arguments)] + pub fn new( + issuer: String, + subject: String, + audience: Audience, + signing_key: SK, + signing_algo: JS, + duration: Duration, + jwt_id_method: RF, + additional_claims: AC, + ) -> Self { + Self { + issuer, + subject, + audience, + signing_key, + signing_algo, + duration, + jwt_id_method, + additional_claims, + include_iat: false, + include_nbf: false, + include_jti: true, + _phantom_jt: PhantomData, + } + } + + /// FIXME: documentation + pub fn set_issuer(mut self, issuer: String) -> Self { + self.issuer = issuer; + self + } + + /// FIXME: documentation + pub fn set_subject(mut self, subject: String) -> Self { + self.subject = subject; + self + } + + /// FIXME: documentation + pub fn set_audience(mut self, audience: Audience) -> Self { + self.audience = audience; + self + } + + /// FIXME: documentation + pub fn set_signing_key(mut self, signing_key: SK) -> Self { + self.signing_key = signing_key; + self + } + + /// FIXME: documentation + pub fn set_signing_algo(mut self, signing_algo: JS) -> Self { + self.signing_algo = signing_algo; + self + } + + /// FIXME: documentation + pub fn set_duration(mut self, duration: Duration) -> Self { + self.duration = duration; + self + } + + /// FIXME: documentation + pub fn set_jwt_id_method(mut self, jwt_id_method: RF) -> Self { + self.jwt_id_method = jwt_id_method; + self + } + + /// FIXME: documentation + pub fn set_additional_claims(mut self, additional_claims: AC) -> Self { + self.additional_claims = additional_claims; + self + } + + /// FIXME: documentation + pub fn include_not_before(mut self, include_nbf: bool) -> Self { + self.include_nbf = include_nbf; + self + } + + /// FIXME: documentation + pub fn include_issued_at(mut self, include_iat: bool) -> Self { + self.include_iat = include_iat; + self + } + + /// FIXME: documentation + pub fn include_jwt_id(mut self, include_jti: bool) -> Self { + self.include_jti = include_jti; + self + } + + /// FIXME: documentation + pub fn build(self) -> Result, JsonWebTokenError> { + let now = chrono::Utc::now(); + + let expiration = now + self.duration; + let not_before = if self.include_nbf { Some(now) } else { None }; + let issued_at = if self.include_iat { Some(now) } else { None }; + let jwt_id = if self.include_jti { + let f = self.jwt_id_method; + Some(f()) + } else { + None + }; + + let claims = ClientAuthTokenClaims { + issuer: self.issuer, + subject: self.subject, + audience: self.audience, + expiration, + not_before, + issued_at, + jwt_id, + additional_claims: self.additional_claims, + }; + + let t = ClientAuthToken::new(claims, &self.signing_key, self.signing_algo).unwrap(); + Ok(t) + } +} + +// #[non_exhaustive] +// pub enum JwsKeyIdMethod { +// X5t(String), +// X509Sha256([u8; 32]), +// KeyId(String), +// X509Url(String), +// X509Sha1(Vec), +// } + +/// OpenID Connect ID token. +/// +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ClientAuthToken< + AC: AdditionalClientAuthTokenClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +>( + #[serde(bound = "AC: AdditionalClientAuthTokenClaims")] + JsonWebToken, JsonWebTokenJsonPayloadSerde>, +); + +impl ClientAuthToken +where + AC: AdditionalClientAuthTokenClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + /// FIXME: documentation + pub fn new( + claims: ClientAuthTokenClaims, + signing_key: &S, + alg: JS, + // access_token: Option<&AccessToken>, + // code: Option<&AuthorizationCode>, + ) -> Result + where + JU: JsonWebKeyUse, + K: JsonWebKey, + S: PrivateSigningKey, + { + JsonWebToken::new(ClientAuthTokenClaims { ..claims }, signing_key, &alg).map(Self) + } +} + +impl ToString for ClientAuthToken +where + AC: AdditionalClientAuthTokenClaims, + JE: JweContentEncryptionAlgorithm, + JS: JwsSigningAlgorithm, + JT: JsonWebKeyType, +{ + fn to_string(&self) -> String { + serde_json::to_value(self) + // This should never arise, since we're just asking serde_json to serialize the + // signing input concatenated with the signature, both of which are precomputed. + .expect("ID token serialization failed") + .as_str() + // This should also never arise, since our IdToken serializer always calls serialize_str + .expect("ID token serializer did not produce a str") + .to_owned() + } +} + +/* +#[cfg(test)] +mod tests { + + use std::time::Duration; + + use oauth2::{reqwest::http_client, AuthUrl}; + + use crate::{ + core::{ + CoreAuthDisplay, CoreClaimName, CoreClaimType, CoreClientAuthMethod, CoreGrantType, + CoreJsonWebKey, CoreJsonWebKeyType, CoreJsonWebKeyUse, + CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, + CoreJwsSigningAlgorithm, CoreResponseMode, CoreRsaPrivateSigningKey, + CoreSubjectIdentifierType, + }, + jwt::tests::TEST_RSA_PRIV_KEY, + AdditionalProviderMetadata, ClaimName, Client, EmptyAdditionalProviderMetadata, + JsonWebKeyId, JsonWebKeySetUrl, ProviderMetadata, ResponseType, ResponseTypes, + }; + + use crate::core::{ + CoreAuthenticationFlow, CoreClient, CoreProviderMetadata, CoreResponseType, + CoreUserInfoClaims, + }; + use crate::{ + AccessTokenHash, AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, + IssuerUrl, Nonce, PkceCodeChallenge, RedirectUrl, Scope, + }; + + use super::*; + use anyhow::anyhow; + + #[derive(Debug, Deserialize, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize)] + pub struct CoreClaimName2(String); + impl ClaimName for CoreClaimName2 {} + + use crate::{OAuth2TokenResponse, TokenResponse}; + + type MicrosoftProviderMetadata = ProviderMetadata< + EmptyAdditionalProviderMetadata, + CoreAuthDisplay, + CoreClientAuthMethod, + CoreClaimName, + CoreClaimType, + CoreGrantType, + CoreJweContentEncryptionAlgorithm, + CoreJweKeyManagementAlgorithm, + CoreJwsSigningAlgorithm, + CoreJsonWebKeyType, + CoreJsonWebKeyUse, + CoreJsonWebKey, + CoreResponseMode, + CoreResponseType, + CoreSubjectIdentifierType, + >; + + #[test] + fn azure_ad_style() -> Result<(), anyhow::Error> { + let private_key = CoreRsaPrivateSigningKey::from_pem( + TEST_RSA_PRIV_KEY, + Some(JsonWebKeyId::new("flyveQx6E1p5crtxOzA64kwjYmo".to_string())), + ) + .unwrap(); + const tenant_id: &str = "3d02d73d-a23a-4989-93ef-ac3c459edabb"; + + let client_id = ClientId::new("9aca7c0e-8e4a-4b36-8c69-1c2323092699".to_string()); + let issuer = IssuerUrl::new( + format!( + "https://login.microsoftonline.com/{}/oauth2/v2.0", + tenant_id + ) + .to_string(), + ) + .unwrap(); + let authorization_endpoint = AuthUrl::new(format!( + "https://login.microsoft.com/{}/oauth2/v2.0/authorize", + tenant_id + )) + .unwrap(); + + let provider_metadata = MicrosoftProviderMetadata::discover( + &IssuerUrl::new(format!( + "https://login.microsoftonline.com/{}/v2.0", + tenant_id + )) + .unwrap(), + http_client, + ) + .unwrap(); + + let client = CoreClient::from_provider_metadata(provider_metadata, client_id, None); + + println!("HERE1"); + + let client_auth_token_builder = client.client_auth_token_builder( + private_key, + CoreJwsSigningAlgorithm::RsaSsaPkcs1V15Sha256, + Duration::from_secs(30), + || ClientAuthTokenId("haha".to_string()), + EmptyAdditionalClientAuthTokenClaims {}, + ); + + let token = client_auth_token_builder.build().unwrap(); + println!("HERE2"); + + let token_response = client + .exchange_client_credential_with_auth_token(token) + .unwrap() + .add_scope(Scope::new( + "https://0fsxp-admin.sharepoint.com/.default".to_string(), + )) + .request(http_client) + .unwrap(); + + println!("HERE3"); + eprintln!("token_response = {:?}", token_response); + + Ok(()) + } +} +*/ diff --git a/src/lib.rs b/src/lib.rs index 5306320..b586f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -663,6 +663,9 @@ mod macros; /// Baseline OpenID Connect implementation and types. pub mod core; +/// FIXME: documentation +pub mod jwt_profile; + /// OpenID Connect Dynamic Client Registration. pub mod registration; @@ -1422,7 +1425,7 @@ where AuthenticationFlow::Implicit(include_token) => { if include_token { OAuth2ResponseType::new( - vec![ + [ core::CoreResponseType::IdToken, core::CoreResponseType::Token, ]